From c624b1dcb9c0871644e3c6248221a7949b8ed074 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 8 Apr 2026 21:04:03 +0200 Subject: [PATCH 1/4] Generator fixes for trimmable typemap - Fix CRC64 algorithm (Jones instead of System.IO.Hashing) - Fix inherited override detection (walk past DoNotGenerateAcw bases) - Fix instrumentation targetPackage - JCW lazy registerNatives for App/Instrumentation types - Sanitize backtick in proxy names - Manifest name promotion and deferred registration propagation - Build targets and task updates - Typed logger interface - Unit test coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Documentation/docs-mobile/messages/index.md | 1 + Documentation/docs-mobile/messages/xa4250.md | 33 ++ .../Generator/AcwMapWriter.cs | 5 +- .../Generator/AndroidEnumConverter.cs | 2 - .../Generator/AssemblyLevelElementBuilder.cs | 72 ++- .../Generator/ComponentElementBuilder.cs | 11 +- .../Generator/JcwJavaSourceGenerator.cs | 38 +- .../Generator/ManifestConstants.cs | 2 - .../Generator/ManifestGenerator.cs | 62 +-- .../Generator/Model/TypeMapAssemblyData.cs | 2 +- .../Generator/ModelBuilder.cs | 4 +- .../Generator/PropertyMapper.cs | 2 - .../ITrimmableTypeMapLogger.cs | 24 + ...rosoft.Android.Sdk.TrimmableTypeMap.csproj | 3 + .../Scanner/AssemblyIndex.cs | 24 +- .../Scanner/AssemblyManifestInfo.cs | 1 + .../Scanner/JavaPeerInfo.cs | 21 +- .../Scanner/JavaPeerScanner.cs | 49 +- .../Scanner/SupportsGLTextureInfo.cs | 6 + .../Scanner/UsesPermissionInfo.cs | 1 + .../TrimmableTypeMapGenerator.cs | 316 +++++++++++- .../TrimmableTypeMapTypes.cs | 38 +- .../PreserveLists/Trimmable.CoreCLR.xml | 9 + ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 7 + ...roid.Sdk.TypeMap.Trimmable.CoreCLR.targets | 74 ++- ...id.Sdk.TypeMap.Trimmable.NativeAOT.targets | 4 + ...soft.Android.Sdk.TypeMap.Trimmable.targets | 195 +++++--- .../Properties/Resources.Designer.cs | 9 + .../Properties/Resources.resx | 5 + .../Tasks/GenerateEmptyTypemapStub.cs | 97 ++++ .../GenerateNativeApplicationConfigSources.cs | 16 +- .../Tasks/GenerateTrimmableTypeMap.cs | 465 ++++++++++++------ .../Tasks/GenerateTrimmableTypeMapTests.cs | 2 - .../Generator/ExportFieldTests.cs | 4 +- .../Generator/FixtureTestBase.cs | 2 +- .../Generator/JcwJavaSourceGeneratorTests.cs | 63 ++- .../Generator/ManifestGeneratorTests.cs | 173 +++++-- .../TrimmableTypeMapGeneratorTests.cs | 306 +++++++++++- .../TypeMapAssemblyGeneratorTests.cs | 69 ++- .../Scanner/AssemblyAttributeScanningTests.cs | 17 + .../Scanner/JavaPeerScannerTests.Behavior.cs | 13 + .../Scanner/JavaPeerScannerTests.cs | 27 + .../Scanner/OverrideDetectionTests.cs | 18 + .../TestFixtures/AssemblyAttributes.cs | 2 + .../TestFixtures/StubAttributes.cs | 9 + .../TestFixtures/TestTypes.cs | 110 ++++- 46 files changed, 2021 insertions(+), 392 deletions(-) create mode 100644 Documentation/docs-mobile/messages/xa4250.md create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SupportsGLTextureInfo.cs create mode 100644 src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml create mode 100644 src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs diff --git a/Documentation/docs-mobile/messages/index.md b/Documentation/docs-mobile/messages/index.md index 0ef4f1febfa..ae77f286553 100644 --- a/Documentation/docs-mobile/messages/index.md +++ b/Documentation/docs-mobile/messages/index.md @@ -207,6 +207,7 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla + [XA4247](xa4247.md): Could not resolve POM file for artifact '{artifact}'. + [XA4248](xa4248.md): Could not find NuGet package '{nugetId}' version '{version}' in lock file. Ensure NuGet Restore has run since this `` was added. + [XA4235](xa4249.md): Maven artifact specification '{artifact}' is invalid. The correct format is 'group_id:artifact_id:version'. ++ [XA4250](xa4250.md): Manifest-referenced type '{type}' was not found in any scanned assembly. It may be a framework type. + XA4300: Native library '{library}' will not be bundled because it has an unsupported ABI. + [XA4301](xa4301.md): Apk already contains the item `xxx`. + [XA4302](xa4302.md): Unhandled exception merging \`AndroidManifest.xml\`: {ex} diff --git a/Documentation/docs-mobile/messages/xa4250.md b/Documentation/docs-mobile/messages/xa4250.md new file mode 100644 index 00000000000..3ef74ddc5e5 --- /dev/null +++ b/Documentation/docs-mobile/messages/xa4250.md @@ -0,0 +1,33 @@ +--- +title: .NET for Android warning XA4250 +description: XA4250 warning code +ms.date: 04/07/2026 +f1_keywords: + - "XA4250" +--- + +# .NET for Android warning XA4250 + +## Example message + +Manifest-referenced type '{0}' was not found in any scanned assembly. It may be a framework type. + +```text +warning XA4250: Manifest-referenced type 'com.example.MainActivity' was not found in any scanned assembly. It may be a framework type. +``` + +## Issue + +The build found a type name in `AndroidManifest.xml`, but it could not match that name to any Java peer discovered in the app's managed assemblies. + +This can be expected for framework-provided types, but it can also indicate that the manifest entry does not match the name generated for a managed Android component. + +## Solution + +If the manifest entry refers to an Android framework type, this warning can usually be ignored. + +Otherwise: + +1. Verify the `android:name` value in the manifest. +2. Ensure the managed type is included in the app build. +3. Check for namespace, `[Register]`, or nested-type naming mismatches between the manifest and the managed type. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs index 8d4005d6390..4314a2acba8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AcwMapWriter.cs @@ -1,4 +1,3 @@ -#nullable enable using System; using System.Collections.Generic; using System.IO; @@ -29,10 +28,10 @@ public static class AcwMapWriter public static void Write (TextWriter writer, IEnumerable peers) { foreach (var peer in peers.OrderBy (p => p.ManagedTypeName, StringComparer.Ordinal)) { - string javaKey = peer.JavaName.Replace ('/', '.'); + string javaKey = JniSignatureHelper.JniNameToJavaName (peer.JavaName); string managedKey = peer.ManagedTypeName; string partialAsmQualifiedName = $"{managedKey}, {peer.AssemblyName}"; - string compatJniName = peer.CompatJniName.Replace ('/', '.'); + string compatJniName = JniSignatureHelper.JniNameToJavaName (peer.CompatJniName); // Line 1: PartialAssemblyQualifiedName;JavaKey writer.Write (partialAsmQualifiedName); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs index b25a1407a4f..6797e32d6d0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AndroidEnumConverter.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Collections.Generic; namespace Microsoft.Android.Sdk.TrimmableTypeMap; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs index 8ba00724f87..a31be12bfef 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/AssemblyLevelElementBuilder.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.Globalization; @@ -21,18 +19,23 @@ internal static void AddAssemblyLevelElements (XElement manifest, XElement app, { var existingPermissions = new HashSet ( manifest.Elements ("permission").Select (e => (string?)e.Attribute (AttName)).OfType ()); + var existingPermissionGroups = new HashSet ( + manifest.Elements ("permission-group").Select (e => (string?)e.Attribute (AttName)).OfType ()); + var existingPermissionTrees = new HashSet ( + manifest.Elements ("permission-tree").Select (e => (string?)e.Attribute (AttName)).OfType ()); var existingUsesPermissions = new HashSet ( manifest.Elements ("uses-permission").Select (e => (string?)e.Attribute (AttName)).OfType ()); // elements foreach (var perm in info.Permissions) { - if (string.IsNullOrEmpty (perm.Name) || existingPermissions.Contains (perm.Name)) { + if (string.IsNullOrEmpty (perm.Name) || !existingPermissions.Add (perm.Name)) { continue; } var element = new XElement ("permission", new XAttribute (AttName, perm.Name)); PropertyMapper.MapDictionaryProperties (element, perm.Properties, "Label", "label"); PropertyMapper.MapDictionaryProperties (element, perm.Properties, "Description", "description"); PropertyMapper.MapDictionaryProperties (element, perm.Properties, "Icon", "icon"); + PropertyMapper.MapDictionaryProperties (element, perm.Properties, "RoundIcon", "roundIcon"); PropertyMapper.MapDictionaryProperties (element, perm.Properties, "PermissionGroup", "permissionGroup"); PropertyMapper.MapDictionaryEnumProperty (element, perm.Properties, "ProtectionLevel", "protectionLevel", AndroidEnumConverter.ProtectionToString); manifest.Add (element); @@ -40,36 +43,41 @@ internal static void AddAssemblyLevelElements (XElement manifest, XElement app, // elements foreach (var pg in info.PermissionGroups) { - if (string.IsNullOrEmpty (pg.Name)) { + if (string.IsNullOrEmpty (pg.Name) || !existingPermissionGroups.Add (pg.Name)) { continue; } var element = new XElement ("permission-group", new XAttribute (AttName, pg.Name)); PropertyMapper.MapDictionaryProperties (element, pg.Properties, "Label", "label"); PropertyMapper.MapDictionaryProperties (element, pg.Properties, "Description", "description"); PropertyMapper.MapDictionaryProperties (element, pg.Properties, "Icon", "icon"); + PropertyMapper.MapDictionaryProperties (element, pg.Properties, "RoundIcon", "roundIcon"); manifest.Add (element); } // elements foreach (var pt in info.PermissionTrees) { - if (string.IsNullOrEmpty (pt.Name)) { + if (string.IsNullOrEmpty (pt.Name) || !existingPermissionTrees.Add (pt.Name)) { continue; } var element = new XElement ("permission-tree", new XAttribute (AttName, pt.Name)); PropertyMapper.MapDictionaryProperties (element, pt.Properties, "Label", "label"); PropertyMapper.MapDictionaryProperties (element, pt.Properties, "Icon", "icon"); + PropertyMapper.MapDictionaryProperties (element, pt.Properties, "RoundIcon", "roundIcon"); manifest.Add (element); } // elements foreach (var up in info.UsesPermissions) { - if (string.IsNullOrEmpty (up.Name) || existingUsesPermissions.Contains (up.Name)) { + if (string.IsNullOrEmpty (up.Name) || !existingUsesPermissions.Add (up.Name)) { continue; } var element = new XElement ("uses-permission", new XAttribute (AttName, up.Name)); if (up.MaxSdkVersion.HasValue) { element.SetAttributeValue (AndroidNs + "maxSdkVersion", up.MaxSdkVersion.Value.ToString (CultureInfo.InvariantCulture)); } + if (!string.IsNullOrEmpty (up.UsesPermissionFlags)) { + element.SetAttributeValue (AndroidNs + "usesPermissionFlags", up.UsesPermissionFlags); + } manifest.Add (element); } @@ -77,7 +85,7 @@ internal static void AddAssemblyLevelElements (XElement manifest, XElement app, var existingFeatures = new HashSet ( manifest.Elements ("uses-feature").Select (e => (string?)e.Attribute (AttName)).OfType ()); foreach (var uf in info.UsesFeatures) { - if (uf.Name is not null && !existingFeatures.Contains (uf.Name)) { + if (uf.Name is not null && existingFeatures.Add (uf.Name)) { var element = new XElement ("uses-feature", new XAttribute (AttName, uf.Name), new XAttribute (AndroidNs + "required", uf.Required ? "true" : "false")); @@ -153,11 +161,59 @@ internal static void AddAssemblyLevelElements (XElement manifest, XElement app, } manifest.Add (element); } + + // elements + var existingGLTextures = new HashSet ( + manifest.Elements ("supports-gl-texture").Select (e => (string?)e.Attribute (AttName)).OfType ()); + foreach (var gl in info.SupportsGLTextures) { + if (existingGLTextures.Add (gl.Name)) { + manifest.Add (new XElement ("supports-gl-texture", new XAttribute (AttName, gl.Name))); + } + } } - internal static void ApplyApplicationProperties (XElement app, Dictionary properties) + internal static void ApplyApplicationProperties ( + XElement app, + Dictionary properties, + IReadOnlyList allPeers, + Action? warn = null) { PropertyMapper.ApplyMappings (app, properties, PropertyMapper.ApplicationPropertyMappings, skipExisting: true); + + // BackupAgent and ManageSpaceActivity are Type properties — resolve managed type names to JNI names + ApplyTypeProperty (app, properties, allPeers, "BackupAgent", "backupAgent", warn); + ApplyTypeProperty (app, properties, allPeers, "ManageSpaceActivity", "manageSpaceActivity", warn); + } + + static void ApplyTypeProperty ( + XElement app, + Dictionary properties, + IReadOnlyList allPeers, + string propertyName, + string xmlAttrName, + Action? warn) + { + if (app.Attribute (AndroidNs + xmlAttrName) is not null) { + return; + } + if (!properties.TryGetValue (propertyName, out var value) || value is not string managedName || managedName.Length == 0) { + return; + } + + // Strip assembly qualification if present (e.g., "MyApp.MyAgent, MyAssembly") + var commaIndex = managedName.IndexOf (','); + if (commaIndex > 0) { + managedName = managedName.Substring (0, commaIndex).Trim (); + } + + foreach (var peer in allPeers) { + if (peer.ManagedTypeName == managedName) { + app.SetAttributeValue (AndroidNs + xmlAttrName, peer.JavaName.Replace ('/', '.')); + return; + } + } + + warn?.Invoke ($"Could not resolve {propertyName} type '{managedName}' to a Java peer for android:{xmlAttrName}."); } internal static void AddInternetPermission (XElement manifest) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs index 9f6f8e7f516..6745e9b2b2e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Globalization; using System.Linq; @@ -157,7 +155,7 @@ internal static XElement CreateMetaDataElement (MetaDataInfo meta) internal static void UpdateApplicationElement (XElement app, JavaPeerInfo peer) { - string jniName = peer.JavaName.Replace ('/', '.'); + string jniName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); app.SetAttributeValue (AttName, jniName); var component = peer.ComponentAttribute; @@ -167,9 +165,9 @@ internal static void UpdateApplicationElement (XElement app, JavaPeerInfo peer) PropertyMapper.ApplyMappings (app, component.Properties, PropertyMapper.ApplicationElementMappings); } - internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer) + internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer, string packageName) { - string jniName = peer.JavaName.Replace ('/', '.'); + string jniName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); var element = new XElement ("instrumentation", new XAttribute (AttName, jniName)); @@ -178,6 +176,9 @@ internal static void AddInstrumentation (XElement manifest, JavaPeerInfo peer) return; } PropertyMapper.ApplyMappings (element, component.Properties, PropertyMapper.InstrumentationMappings); + if (element.Attribute (AndroidNs + "targetPackage") is null && !string.IsNullOrEmpty (packageName)) { + element.SetAttributeValue (AndroidNs + "targetPackage", packageName); + } manifest.Add (element); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs index 96bd0f729ab..d629fc83d2c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JcwJavaSourceGenerator.cs @@ -122,14 +122,28 @@ static void WriteClassDeclaration (JavaPeerInfo type, TextWriter writer) static void WriteStaticInitializer (JavaPeerInfo type, TextWriter writer) { + string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName); + // Application and Instrumentation types cannot call registerNatives in their - // static initializer — the native library isn't loaded yet at that point. - // Their registration is deferred to ApplicationRegistration.registerApplications(). + // static initializer — the runtime isn't ready yet at that point. Emit a + // lazy one-time helper instead so the first managed callback can register + // the class just before invoking its native method. if (type.CannotRegisterInStaticConstructor) { + writer.Write ($$""" + private static boolean __md_natives_registered; + private static synchronized void __md_registerNatives () + { + if (!__md_natives_registered) { + mono.android.Runtime.registerNatives ({{className}}.class); + __md_natives_registered = true; + } + } + + +"""); return; } - string className = JniSignatureHelper.GetJavaSimpleName (type.JavaName); writer.Write ($$""" static { mono.android.Runtime.registerNatives ({{className}}.class); @@ -153,7 +167,17 @@ static void WriteConstructors (JavaPeerInfo type, TextWriter writer) public {{simpleClassName}} ({{parameters}}) { super ({{superArgs}}); + +"""); + + if (!type.CannotRegisterInStaticConstructor) { + writer.Write ($$""" if (getClass () == {{simpleClassName}}.class) nctor_{{ctor.ConstructorIndex}} ({{args}}); + +"""); + } + + writer.Write ($$""" } @@ -196,6 +220,10 @@ static void WriteFields (JavaPeerInfo type, TextWriter writer) static void WriteMethods (JavaPeerInfo type, TextWriter writer) { + string registerNativesLine = type.CannotRegisterInStaticConstructor + ? "\t\t__md_registerNatives ();\n" + : ""; + foreach (var method in type.MarshalMethods) { if (method.IsConstructor) { continue; @@ -221,7 +249,7 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) @Override public {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}} { - {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); +{{registerNativesLine}} {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); } public native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}}); @@ -232,7 +260,7 @@ static void WriteMethods (JavaPeerInfo type, TextWriter writer) {{access}} {{javaReturnType}} {{method.JniName}} ({{parameters}}){{throwsClause}} { - {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); +{{registerNativesLine}} {{returnPrefix}}{{method.NativeCallbackName}} ({{args}}); } {{access}} native {{javaReturnType}} {{method.NativeCallbackName}} ({{parameters}}); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs index 4ed62e9baa6..191637babaa 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestConstants.cs @@ -1,5 +1,3 @@ -#nullable enable - using System.Xml.Linq; namespace Microsoft.Android.Sdk.TrimmableTypeMap; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 1bea7928e30..9f64a305023 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Xml.Linq; @@ -23,9 +22,9 @@ class ManifestGenerator public string ApplicationLabel { get; set; } = ""; public string VersionCode { get; set; } = ""; public string VersionName { get; set; } = ""; - public string MinSdkVersion { get; set; } = "21"; - public string TargetSdkVersion { get; set; } = "36"; - public string AndroidRuntime { get; set; } = "coreclr"; + public string MinSdkVersion { get; set; } = ""; + public string TargetSdkVersion { get; set; } = ""; + public string RuntimeProviderJavaName { get; set; } = ""; public bool Debug { get; set; } public bool NeedsInternet { get; set; } public bool EmbedAssemblies { get; set; } @@ -33,16 +32,16 @@ class ManifestGenerator public bool ForceExtractNativeLibs { get; set; } public string? ManifestPlaceholders { get; set; } public string? ApplicationJavaClass { get; set; } + public Action? Warn { get; set; } /// /// Generates the merged manifest from an optional pre-loaded template and writes it to . /// Returns the list of additional content provider names (for ApplicationRegistration.java). /// - public IList Generate ( + public (XDocument Document, IList ProviderNames) Generate ( XDocument? manifestTemplate, IReadOnlyList allPeers, - AssemblyManifestInfo assemblyInfo, - string outputPath) + AssemblyManifestInfo assemblyInfo) { var doc = manifestTemplate ?? CreateDefaultManifest (); var manifest = doc.Root; @@ -55,7 +54,7 @@ public IList Generate ( // Apply assembly-level [Application] properties if (assemblyInfo.ApplicationProperties is not null) { - AssemblyLevelElementBuilder.ApplyApplicationProperties (app, assemblyInfo.ApplicationProperties); + AssemblyLevelElementBuilder.ApplyApplicationProperties (app, assemblyInfo.ApplicationProperties, allPeers, Warn); } var existingTypes = new HashSet ( @@ -74,11 +73,11 @@ public IList Generate ( } if (peer.ComponentAttribute.Kind == ComponentKind.Instrumentation) { - ComponentElementBuilder.AddInstrumentation (manifest, peer); + ComponentElementBuilder.AddInstrumentation (manifest, peer, PackageName); continue; } - string jniName = peer.JavaName.Replace ('/', '.'); + string jniName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); if (existingTypes.Contains (jniName)) { continue; } @@ -117,19 +116,9 @@ public IList Generate ( } // Apply manifest placeholders - string? placeholders = ManifestPlaceholders; - if (placeholders is not null && placeholders.Length > 0) { - ApplyPlaceholders (doc, placeholders); - } - - // Write output - var outputDir = Path.GetDirectoryName (outputPath); - if (outputDir is not null) { - Directory.CreateDirectory (outputDir); - } - doc.Save (outputPath); + ApplyPlaceholders (doc, ManifestPlaceholders); - return providerNames; + return (doc, providerNames); } XDocument CreateDefaultManifest () @@ -161,6 +150,12 @@ void EnsureManifestAttributes (XElement manifest) // Add if (!manifest.Elements ("uses-sdk").Any ()) { + if (MinSdkVersion.IsNullOrEmpty ()) { + throw new InvalidOperationException ("MinSdkVersion must be provided by MSBuild."); + } + if (TargetSdkVersion.IsNullOrEmpty ()) { + throw new InvalidOperationException ("TargetSdkVersion must be provided by MSBuild."); + } manifest.AddFirst (new XElement ("uses-sdk", new XAttribute (AndroidNs + "minSdkVersion", MinSdkVersion), new XAttribute (AndroidNs + "targetSdkVersion", TargetSdkVersion))); @@ -184,16 +179,19 @@ XElement EnsureApplicationElement (XElement manifest) IList AddRuntimeProviders (XElement app) { - string packageName = "mono"; - string className = "MonoRuntimeProvider"; - - if (string.Equals (AndroidRuntime, "nativeaot", StringComparison.OrdinalIgnoreCase)) { - packageName = "net.dot.jni.nativeaot"; - className = "NativeAotRuntimeProvider"; + if (RuntimeProviderJavaName.IsNullOrEmpty ()) { + throw new InvalidOperationException ("RuntimeProviderJavaName must be provided by MSBuild."); + } + int lastDot = RuntimeProviderJavaName.LastIndexOf ('.'); + if (lastDot < 0 || lastDot == RuntimeProviderJavaName.Length - 1) { + throw new InvalidOperationException ($"RuntimeProviderJavaName must be a fully-qualified Java type name: '{RuntimeProviderJavaName}'."); } + string packageName = RuntimeProviderJavaName.Substring (0, lastDot); + string className = RuntimeProviderJavaName.Substring (lastDot + 1); + // Check if runtime provider already exists in template - string runtimeProviderName = $"{packageName}.{className}"; + string runtimeProviderName = RuntimeProviderJavaName; if (!app.Elements ("provider").Any (p => { var name = (string?)p.Attribute (ManifestConstants.AttName); return name == runtimeProviderName || @@ -249,8 +247,12 @@ XElement CreateRuntimeProvider (string name, string? processName, int initOrder) /// Replaces ${key} placeholders in all attribute values throughout the document. /// Placeholder format: "key1=value1;key2=value2" /// - static void ApplyPlaceholders (XDocument doc, string placeholders) + internal static void ApplyPlaceholders (XDocument doc, string? placeholders) { + if (placeholders.IsNullOrEmpty ()) { + return; + } + var replacements = new Dictionary (StringComparer.Ordinal); foreach (var entry in placeholders.Split (PlaceholderSeparators, StringSplitOptions.RemoveEmptyEntries)) { var eqIndex = entry.IndexOf ('='); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 56484f7a99a..42cf48447ee 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -182,7 +182,7 @@ sealed record UcoMethodData /// An [UnmanagedCallersOnly] static wrapper for a constructor callback. /// Signature must match the full JNI native method signature (jnienv + self + ctor params) /// so the ABI is correct when JNI dispatches the call. -/// Body: TrimmableNativeRegistration.ActivateInstance(self, typeof(TargetType)). +/// Body: TrimmableTypeMap.ActivateInstance(self, typeof(TargetType)). /// sealed record UcoConstructorData { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 3cb37a217d6..1fbc0ad276d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -173,7 +173,9 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, HashSet used { // Use managed type name for proxy naming to guarantee uniqueness across aliases // (two types with the same JNI name will have different managed names). - var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_') + "_Proxy"; + // Replace generic arity markers too, because backticks would make the emitted + // proxy type itself look generic even though we don't emit generic parameters. + var proxyTypeName = peer.ManagedTypeName.Replace ('.', '_').Replace ('+', '_').Replace ('`', '_') + "_Proxy"; // Guard against name collisions (e.g., "My.Type" and "My_Type" both map to "My_Type_Proxy") if (!usedProxyNames.Add (proxyTypeName)) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs index 32b07671600..9610941d829 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PropertyMapper.cs @@ -1,5 +1,3 @@ -#nullable enable - using System; using System.Collections.Generic; using System.Globalization; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs new file mode 100644 index 00000000000..8224e2d7427 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -0,0 +1,24 @@ +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +public interface ITrimmableTypeMapLogger +{ + void LogNoJavaPeerTypesFound (); + + void LogJavaPeerScanInfo (int assemblyCount, int peerCount); + + void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount); + + void LogDeferredRegistrationTypesInfo (int typeCount); + + void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount); + + void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount); + + void LogGeneratedTypeMapAssembliesInfo (int assemblyCount); + + void LogGeneratedJcwFilesInfo (int sourceCount); + + void LogUnresolvedTypeWarning (string name); + + void LogRootingManifestReferencedTypeInfo (string name, string managedTypeName); +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj index 249bdc8def1..99399656317 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Microsoft.Android.Sdk.TrimmableTypeMap.csproj @@ -5,6 +5,7 @@ $(TargetFrameworkNETStandard) enable Nullable + true Microsoft.Android.Sdk.TrimmableTypeMap true ..\..\product.snk @@ -18,6 +19,8 @@ + + diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index e63dcc63202..190fd095d8c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -343,13 +343,27 @@ static bool TryGetNamedArgument (CustomAttributeValue value, string a /// /// Scans assembly-level custom attributes for manifest-related data. /// + static readonly HashSet KnownAssemblyAttributes = new (StringComparer.Ordinal) { + "PermissionAttribute", + "PermissionGroupAttribute", + "PermissionTreeAttribute", + "UsesPermissionAttribute", + "UsesFeatureAttribute", + "UsesLibraryAttribute", + "UsesConfigurationAttribute", + "MetaDataAttribute", + "PropertyAttribute", + "SupportsGLTextureAttribute", + "ApplicationAttribute", + }; + internal void ScanAssemblyAttributes (AssemblyManifestInfo info) { var asmDef = Reader.GetAssemblyDefinition (); foreach (var caHandle in asmDef.GetCustomAttributes ()) { var ca = Reader.GetCustomAttribute (caHandle); var attrName = GetCustomAttributeName (ca, Reader); - if (attrName is null) { + if (attrName is null || !KnownAssemblyAttributes.Contains (attrName)) { continue; } @@ -383,6 +397,11 @@ internal void ScanAssemblyAttributes (AssemblyManifestInfo info) case "PropertyAttribute": info.Properties.Add (CreatePropertyInfo (name, props)); break; + case "SupportsGLTextureAttribute": + if (name.Length > 0) { + info.SupportsGLTextures.Add (new SupportsGLTextureInfo { Name = name }); + } + break; case "ApplicationAttribute": info.ApplicationProperties ??= new Dictionary (StringComparer.Ordinal); foreach (var kvp in props) { @@ -421,7 +440,8 @@ internal void ScanAssemblyAttributes (AssemblyManifestInfo info) static UsesPermissionInfo CreateUsesPermissionInfo (string name, Dictionary props) { int? maxSdk = props.TryGetValue ("MaxSdkVersion", out var v) && v is int max ? max : null; - return new UsesPermissionInfo { Name = name, MaxSdkVersion = maxSdk }; + string? flags = props.TryGetValue ("UsesPermissionFlags", out var f) && f is string s ? s : null; + return new UsesPermissionInfo { Name = name, MaxSdkVersion = maxSdk, UsesPermissionFlags = flags }; } static UsesFeatureInfo CreateUsesFeatureInfo (string name, Dictionary props) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs index 068aa226709..ea917f72867 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyManifestInfo.cs @@ -13,6 +13,7 @@ internal sealed class AssemblyManifestInfo public List UsesConfigurations { get; } = []; public List MetaData { get; } = []; public List Properties { get; } = []; + public List SupportsGLTextures { get; } = []; public Dictionary? ApplicationProperties { get; set; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index aed62453042..ad4c6b5178c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -12,9 +12,11 @@ public sealed record JavaPeerInfo { /// /// JNI type name, e.g., "android/app/Activity". - /// Extracted from the [Register] attribute. + /// Extracted from the [Register] attribute or auto-computed during scanning. + /// Manifest rooting may later promote this to when + /// a component is referenced by its managed-namespace form. /// - public required string JavaName { get; init; } + public required string JavaName { get; set; } /// /// Compat JNI type name, e.g., "myapp.namespace/MyType" for user types (uses raw namespace, not CRC64). @@ -48,7 +50,7 @@ public sealed record JavaPeerInfo /// that extends Activity. Null for java/lang/Object or types without a Java base. /// Needed by JCW Java source generation ("extends" clause). /// - public string? BaseJavaName { get; init; } + public string? BaseJavaName { get; set; } /// /// JNI names of Java interfaces this type implements, e.g., ["android/view/View$OnClickListener"]. @@ -69,16 +71,23 @@ public sealed record JavaPeerInfo /// Types with component attributes ([Activity], [Service], etc.), /// custom views from layout XML, or manifest-declared components /// are unconditionally preserved (not trimmable). + /// May be set to true after scanning when the manifest references a type + /// that the scanner did not mark as unconditional. Should only ever be set + /// to true, never back to false. /// - public bool IsUnconditional { get; init; } + public bool IsUnconditional { get; set; } /// - /// True for Application and Instrumentation types. These types cannot call + /// True for Application and Instrumentation types, plus any generated managed + /// base classes they rely on during startup. These types cannot call /// registerNatives in their static initializer because the native library /// (libmonodroid.so) is not loaded until after the Application class is instantiated. /// Registration is deferred to ApplicationRegistration.registerApplications(). + /// This may also be set after scanning when a type is only discovered from + /// manifest android:name usage on <application> or + /// <instrumentation>. /// - public bool CannotRegisterInStaticConstructor { get; init; } + public bool CannotRegisterInStaticConstructor { get; set; } /// /// Marshal methods: methods with [Register(name, sig, connector)], [Export], or diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 4b298352b75..fdaecc46077 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -6,6 +6,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using Java.Interop.Tools.JavaCallableWrappers; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -246,7 +247,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, IsGenericDefinition = isGenericDefinition, - ComponentAttribute = ToComponentInfo (attrInfo, typeDef, index), + ComponentAttribute = ToComponentInfo (attrInfo), }; results [fullName] = peer; @@ -732,7 +733,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, (RegisterInfo Info, string DeclaringTypeName, string DeclaringAssemblyName)? FindBaseRegisteredMethodInfo ( TypeDefinition typeDef, AssemblyIndex index, string methodName, MethodDefinition derivedMethod) { - if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out var baseHandle, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { + if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out _, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { return null; } @@ -760,10 +761,8 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, } } - // Recurse up the hierarchy (stop at DoNotGenerateAcw boundary) - if (baseIndex.RegisterInfoByType.TryGetValue (baseHandle, out var baseRegInfo) && baseRegInfo.DoNotGenerateAcw) { - return null; - } + // Keep walking the full base hierarchy so overrides can inherit [Register] + // metadata declared above an intermediate MCW base type. return FindBaseRegisteredMethodInfo (baseTypeDef, baseIndex, methodName, derivedMethod); } @@ -796,7 +795,7 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, MarshalMethodInfo? FindBaseRegisteredProperty (TypeDefinition typeDef, AssemblyIndex index, string getterName, MethodDefinition derivedGetter) { - if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out var baseHandle, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { + if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out _, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { return null; } @@ -835,10 +834,8 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, } } - // Recurse up (stop at DoNotGenerateAcw boundary) - if (baseIndex.RegisterInfoByType.TryGetValue (baseHandle, out var baseRegInfo) && baseRegInfo.DoNotGenerateAcw) { - return null; - } + // Keep walking the full base hierarchy so property overrides can inherit + // [Register] metadata declared above an intermediate MCW base type. return FindBaseRegisteredProperty (baseTypeDef, baseIndex, getterName, derivedGetter); } @@ -956,7 +953,7 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; } - static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) + bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) { exportInfo = null; foreach (var caHandle in methodDef.GetCustomAttributes ()) { @@ -1006,7 +1003,7 @@ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex return null; } - static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportAttribute (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) { var value = index.DecodeAttribute (ca); @@ -1050,7 +1047,7 @@ static bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex ); } - static string BuildJniSignatureFromManaged (MethodSignature sig) + string BuildJniSignatureFromManaged (MethodSignature sig) { var sb = new System.Text.StringBuilder (); sb.Append ('('); @@ -1067,7 +1064,7 @@ static string BuildJniSignatureFromManaged (MethodSignature sig) /// [ExportField] methods use the managed method name as the JNI name and have /// a connector of "__export__" (matching legacy CecilImporter behavior). /// - static (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportFieldAsMethod (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) + (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportFieldAsMethod (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) { var managedName = index.Reader.GetString (methodDef.Name); var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); @@ -1080,10 +1077,11 @@ static string BuildJniSignatureFromManaged (MethodSignature sig) } /// - /// Maps a managed type name to its JNI descriptor. Falls back to - /// "Ljava/lang/Object;" for unknown types (used by [Export] signature computation). + /// Maps a managed type name to its JNI descriptor. Resolves Java-bound types + /// via their [Register] attribute, falling back to "Ljava/lang/Object;" only + /// for types that cannot be resolved (used by [Export] signature computation). /// - static string ManagedTypeToJniDescriptor (string managedType) + string ManagedTypeToJniDescriptor (string managedType) { var primitive = TryGetPrimitiveJniDescriptor (managedType); if (primitive is not null) { @@ -1094,6 +1092,12 @@ static string ManagedTypeToJniDescriptor (string managedType) return $"[{ManagedTypeToJniDescriptor (managedType.Substring (0, managedType.Length - 2))}"; } + // Try to resolve as a Java peer type with [Register] + var resolved = TryResolveJniObjectDescriptor (managedType); + if (resolved is not null) { + return resolved; + } + return "Ljava/lang/Object;"; } @@ -1470,8 +1474,11 @@ static string GetCrc64PackageName (string ns, string assemblyName) return ns.ToLowerInvariant ().Replace ('.', '/'); } + // Keep this in sync with JavaNativeTypeManager.ToJniName(Type)/(TypeDefinition). + // The trimmable build path must emit the exact same CRC64 package names that the + // runtime later computes for FindClass(Type) and peer activation. var data = System.Text.Encoding.UTF8.GetBytes ($"{ns}:{assemblyName}"); - var hash = System.IO.Hashing.Crc64.Hash (data); + var hash = Crc64Helper.Compute (data); return $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}"; } @@ -1515,7 +1522,7 @@ static List BuildJavaConstructors (List /// Checks a single method for [ExportField] and adds a JavaFieldInfo if found. /// Called inline during Pass 1 to avoid a separate iteration. /// - static void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List fields) + void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List fields) { foreach (var caHandle in methodDef.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); @@ -1553,7 +1560,7 @@ static void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, } } - static ComponentInfo? ToComponentInfo (TypeAttributeInfo? attrInfo, TypeDefinition typeDef, AssemblyIndex index) + static ComponentInfo? ToComponentInfo (TypeAttributeInfo? attrInfo) { if (attrInfo is null) { return null; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SupportsGLTextureInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SupportsGLTextureInfo.cs new file mode 100644 index 00000000000..f1db452a2a8 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SupportsGLTextureInfo.cs @@ -0,0 +1,6 @@ +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +internal sealed record SupportsGLTextureInfo +{ + public required string Name { get; init; } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs index d11df2d8a3f..bf09a3dfee2 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/UsesPermissionInfo.cs @@ -4,4 +4,5 @@ internal sealed record UsesPermissionInfo { public required string Name { get; init; } public int? MaxSdkVersion { get; init; } + public string? UsesPermissionFlags { get; init; } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 1a2b0691057..c79e6f33cab 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -3,48 +3,111 @@ using System.IO; using System.Linq; using System.Reflection.PortableExecutable; +using System.Xml.Linq; namespace Microsoft.Android.Sdk.TrimmableTypeMap; public class TrimmableTypeMapGenerator { - readonly Action log; + readonly ITrimmableTypeMapLogger? logger; - public TrimmableTypeMapGenerator (Action log) + public TrimmableTypeMapGenerator (ITrimmableTypeMapLogger? logger = null) { - this.log = log ?? throw new ArgumentNullException (nameof (log)); + this.logger = logger; } + /// + /// Runs the full generation pipeline: scan assemblies, generate typemap + /// assemblies, generate JCW Java sources, and optionally generate a merged manifest. + /// No file IO is performed — all results are returned in memory. + /// public TrimmableTypeMapResult Execute ( IReadOnlyList<(string Name, PEReader Reader)> assemblies, Version systemRuntimeVersion, - HashSet frameworkAssemblyNames) + HashSet frameworkAssemblyNames, + ManifestConfig? manifestConfig = null, + XDocument? manifestTemplate = null) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); _ = frameworkAssemblyNames ?? throw new ArgumentNullException (nameof (frameworkAssemblyNames)); - var allPeers = ScanAssemblies (assemblies); + var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies); if (allPeers.Count == 0) { - log ("No Java peer types found, skipping typemap generation."); + logger?.LogNoJavaPeerTypesFound (); return new TrimmableTypeMapResult ([], [], allPeers); } + RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); + PropagateDeferredRegistrationToManagedBaseTypes (allPeers); + var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); - log ($"Generating JCW files for {jcwPeers.Count} types (filtered from {allPeers.Count} total)."); + logger?.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); var generatedJavaSources = GenerateJcwJavaSources (jcwPeers); - return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers); + + // Collect Application/Instrumentation types that need deferred registerNatives + var appRegTypes = allPeers + .Where (p => p.CannotRegisterInStaticConstructor && !p.DoNotGenerateAcw) + .Select (p => JniSignatureHelper.JniNameToJavaName (p.JavaName)) + .Distinct (StringComparer.Ordinal) + .ToList (); + if (appRegTypes.Count > 0) { + logger?.LogDeferredRegistrationTypesInfo (appRegTypes.Count); + } + + var manifest = manifestConfig is not null + ? GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplate) + : null; + + return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest, appRegTypes); + } + + GeneratedManifest GenerateManifest (List allPeers, AssemblyManifestInfo assemblyManifestInfo, + ManifestConfig config, XDocument? manifestTemplate) + { + string minSdk = config.SupportedOSPlatformVersion ?? throw new InvalidOperationException ("SupportedOSPlatformVersion must be provided by MSBuild."); + if (Version.TryParse (minSdk, out var sopv)) { + minSdk = sopv.Major.ToString (System.Globalization.CultureInfo.InvariantCulture); + } + + string targetSdk = config.AndroidApiLevel ?? throw new InvalidOperationException ("AndroidApiLevel must be provided by MSBuild."); + if (Version.TryParse (targetSdk, out var apiVersion)) { + targetSdk = apiVersion.Major.ToString (System.Globalization.CultureInfo.InvariantCulture); + } + + bool forceDebuggable = !config.CheckedBuild.IsNullOrEmpty (); + + var generator = new ManifestGenerator { + PackageName = config.PackageName, + ApplicationLabel = config.ApplicationLabel ?? config.PackageName, + VersionCode = config.VersionCode ?? "", + VersionName = config.VersionName ?? "", + MinSdkVersion = minSdk, + TargetSdkVersion = targetSdk, + RuntimeProviderJavaName = config.RuntimeProviderJavaName ?? throw new InvalidOperationException ("RuntimeProviderJavaName must be provided by MSBuild."), + Debug = config.Debug, + NeedsInternet = config.NeedsInternet, + EmbedAssemblies = config.EmbedAssemblies, + ForceDebuggable = forceDebuggable, + ForceExtractNativeLibs = forceDebuggable, + ManifestPlaceholders = config.ManifestPlaceholders, + ApplicationJavaClass = config.ApplicationJavaClass, + }; + + var (doc, providerNames) = generator.Generate (manifestTemplate, allPeers, assemblyManifestInfo); + return new GeneratedManifest (doc, providerNames.Count > 0 ? providerNames.ToArray () : []); } - List ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies) + (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies) { using var scanner = new JavaPeerScanner (); var peers = scanner.Scan (assemblies); - log ($"Scanned {assemblies.Count} assemblies, found {peers.Count} Java peer types."); - return peers; + var manifestInfo = scanner.ScanAssemblyManifestInfo (); + logger?.LogJavaPeerScanInfo (assemblies.Count, peers.Count); + return (peers, manifestInfo); } List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion) @@ -61,15 +124,15 @@ List GenerateTypeMapAssemblies (List allPeers, generator.Generate (peers, stream, assemblyName); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (assemblyName, stream)); - log ($" {assemblyName}: {peers.Count} types"); + logger?.LogGeneratedTypeMapAssemblyInfo (assemblyName, peers.Count); } var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); rootGenerator.Generate (perAssemblyNames, rootStream); rootStream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); - log ($" Root: {perAssemblyNames.Count} per-assembly refs"); - log ($"Generated {generatedAssemblies.Count} typemap assemblies."); + logger?.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); + logger?.LogGeneratedTypeMapAssembliesInfo (generatedAssemblies.Count); return generatedAssemblies; } @@ -77,7 +140,230 @@ List GenerateJcwJavaSources (List allPeers) { var jcwGenerator = new JcwJavaSourceGenerator (); var sources = jcwGenerator.GenerateContent (allPeers); - log ($"Generated {sources.Count} JCW Java source files."); + logger?.LogGeneratedJcwFilesInfo (sources.Count); return sources.ToList (); } + + internal void RootManifestReferencedTypes (List allPeers, XDocument? doc) + { + if (doc?.Root is not { } root) { + return; + } + + XNamespace androidNs = "http://schemas.android.com/apk/res/android"; + XName attName = androidNs + "name"; + var packageName = (string?) root.Attribute ("package") ?? ""; + + var componentNames = new HashSet (StringComparer.Ordinal); + var deferredRegistrationNames = new HashSet (StringComparer.Ordinal); + foreach (var element in root.Descendants ()) { + switch (element.Name.LocalName) { + case "application": + case "activity": + case "instrumentation": + case "service": + case "receiver": + case "provider": + var name = (string?) element.Attribute (attName); + if (name is not null) { + var resolvedName = ResolveManifestClassName (name, packageName); + componentNames.Add (resolvedName); + + if (element.Name.LocalName is "application" or "instrumentation") { + deferredRegistrationNames.Add (resolvedName); + } + } + break; + } + } + + if (componentNames.Count == 0) { + return; + } + + // Build lookup by both Java and compat dot-names. Keep '$' for nested types, + // because manifests commonly use '$', but also include the Java source form. + var peersByDotName = new Dictionary> (StringComparer.Ordinal); + foreach (var peer in allPeers) { + AddJniLookupNames (peersByDotName, peer.JavaName, peer); + + if (peer.CompatJniName != peer.JavaName) { + AddJniLookupNames (peersByDotName, peer.CompatJniName, peer); + } + } + + foreach (var name in componentNames) { + if (peersByDotName.TryGetValue (name, out var peers)) { + foreach (var peer in peers.Distinct ()) { + PromoteManifestCompatibleJavaName (allPeers, peer, name); + + if (deferredRegistrationNames.Contains (name)) { + peer.CannotRegisterInStaticConstructor = true; + } + + if (!peer.IsUnconditional) { + peer.IsUnconditional = true; + logger?.LogRootingManifestReferencedTypeInfo (name, peer.ManagedTypeName); + } + } + } else { + logger?.LogUnresolvedTypeWarning (name); + } + } + } + + static void PromoteManifestCompatibleJavaName (List allPeers, JavaPeerInfo peer, string manifestName) + { + if (peer.JavaName == peer.CompatJniName || !MatchesManifestName (peer.CompatJniName, manifestName)) { + return; + } + + var promotedJavaName = NormalizeJniName (peer.CompatJniName); + var previousJavaName = peer.JavaName; + if (promotedJavaName == previousJavaName) { + return; + } + + peer.JavaName = promotedJavaName; + + foreach (var candidate in allPeers) { + if (candidate.BaseJavaName == previousJavaName) { + candidate.BaseJavaName = promotedJavaName; + } + } + } + + void PropagateDeferredRegistrationToManagedBaseTypes (List allPeers) + { + var peersByJavaName = new Dictionary (StringComparer.Ordinal); + foreach (var peer in allPeers) { + if (!peersByJavaName.ContainsKey (peer.JavaName)) { + peersByJavaName.Add (peer.JavaName, peer); + } + if (peer.CompatJniName != peer.JavaName && !peersByJavaName.ContainsKey (peer.CompatJniName)) { + peersByJavaName.Add (peer.CompatJniName, peer); + } + } + + foreach (var peer in allPeers.Where (p => p.CannotRegisterInStaticConstructor)) { + PropagateDeferredRegistrationToManagedBaseTypes (peer, peersByJavaName); + } + } + + void PropagateDeferredRegistrationToManagedBaseTypes (JavaPeerInfo peer, Dictionary peersByJavaName) + { + var visited = new HashSet (StringComparer.Ordinal); + var baseJavaName = peer.BaseJavaName; + + while (!baseJavaName.IsNullOrEmpty () && visited.Add (baseJavaName)) { + if (!peersByJavaName.TryGetValue (baseJavaName, out var basePeer) || basePeer.DoNotGenerateAcw) { + break; + } + + basePeer.CannotRegisterInStaticConstructor = true; + basePeer.IsUnconditional = true; + + baseJavaName = basePeer.BaseJavaName; + } + } + + static void AddPeerByDotName (Dictionary> peersByDotName, string dotName, JavaPeerInfo peer) + { + if (!peersByDotName.TryGetValue (dotName, out var list)) { + list = []; + peersByDotName [dotName] = list; + } + + list.Add (peer); + } + + static XDocument? PrepareManifestForRooting (XDocument? manifestTemplate, ManifestConfig? manifestConfig) + { + if (manifestTemplate is null && manifestConfig is null) { + return null; + } + + var doc = manifestTemplate is not null + ? new XDocument (manifestTemplate) + : new XDocument ( + new XElement ( + "manifest", + new XAttribute (XNamespace.Xmlns + "android", ManifestConstants.AndroidNs.NamespaceName))); + + if (doc.Root is not { } root) { + return doc; + } + + if (manifestConfig is null) { + return doc; + } + + if (((string?) root.Attribute ("package")).IsNullOrEmpty () && !manifestConfig.PackageName.IsNullOrEmpty ()) { + root.SetAttributeValue ("package", manifestConfig.PackageName); + } + + ManifestGenerator.ApplyPlaceholders (doc, manifestConfig.ManifestPlaceholders); + + if (!manifestConfig.ApplicationJavaClass.IsNullOrEmpty ()) { + var app = root.Element ("application"); + if (app is null) { + app = new XElement ("application"); + root.Add (app); + } + + if (app.Attribute (ManifestConstants.AttName) is null) { + app.SetAttributeValue (ManifestConstants.AttName, manifestConfig.ApplicationJavaClass); + } + } + + return doc; +} + + static bool MatchesManifestName (string jniOrJavaName, string manifestName) + { + var normalizedName = NormalizeJniName (jniOrJavaName); + var simpleName = JniSignatureHelper.GetJavaSimpleName (normalizedName); + var packageName = JniSignatureHelper.GetJavaPackageName (normalizedName); + var manifestStyleName = packageName.IsNullOrEmpty () ? simpleName : packageName + "." + simpleName; + + return manifestStyleName == manifestName || JniSignatureHelper.JniNameToJavaName (normalizedName) == manifestName; + } + + static string NormalizeJniName (string jniOrJavaName) + { + return jniOrJavaName.IndexOf ('/') >= 0 + ? jniOrJavaName + : jniOrJavaName.Replace ('.', '/'); + } + + static void AddJniLookupNames (Dictionary> peersByDotName, string jniName, JavaPeerInfo peer) + { + var simpleName = JniSignatureHelper.GetJavaSimpleName (jniName); + var packageName = JniSignatureHelper.GetJavaPackageName (jniName); + var manifestName = packageName.IsNullOrEmpty () ? simpleName : packageName + "." + simpleName; + AddPeerByDotName (peersByDotName, manifestName, peer); + + var javaSourceName = JniSignatureHelper.JniNameToJavaName (jniName); + if (javaSourceName != manifestName) { + AddPeerByDotName (peersByDotName, javaSourceName, peer); + } + } + + /// + /// Resolves an android:name value to a fully-qualified class name. + /// Names starting with '.' are relative to the package. Names with no '.' at all + /// are also treated as relative (Android tooling convention). + /// + static string ResolveManifestClassName (string name, string packageName) + { + if (name.StartsWith (".", StringComparison.Ordinal)) { + return packageName + name; + } + + if (name.IndexOf ('.') < 0 && !packageName.IsNullOrEmpty ()) { + return packageName + "." + name; + } + + return name; + } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs index c10a2482bd2..2e162d0d61b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -1,13 +1,49 @@ using System.Collections.Generic; using System.IO; +using System.Xml.Linq; namespace Microsoft.Android.Sdk.TrimmableTypeMap; public record TrimmableTypeMapResult ( IReadOnlyList GeneratedAssemblies, IReadOnlyList GeneratedJavaSources, - IReadOnlyList AllPeers); + IReadOnlyList AllPeers, + GeneratedManifest? Manifest = null, + IReadOnlyList? ApplicationRegistrationTypes = null) +{ + /// + /// Java class names (dot-separated) of Application/Instrumentation types + /// that need deferred Runtime.registerNatives() calls in + /// ApplicationRegistration.registerApplications(). + /// + public IReadOnlyList ApplicationRegistrationTypes { get; init; } = + ApplicationRegistrationTypes ?? []; +} public record GeneratedAssembly (string Name, MemoryStream Content); public record GeneratedJavaSource (string RelativePath, string Content); + +/// +/// The in-memory result of manifest generation: the merged document and +/// any additional content provider class names for ApplicationRegistration.java. +/// +public record GeneratedManifest (XDocument Document, string[] AdditionalProviderSources); + +/// +/// Configuration values for manifest generation. Passed from MSBuild properties. +/// +public record ManifestConfig ( + string PackageName, + string? ApplicationLabel = null, + string? VersionCode = null, + string? VersionName = null, + string? AndroidApiLevel = null, + string? SupportedOSPlatformVersion = null, + string? RuntimeProviderJavaName = null, + bool Debug = false, + bool NeedsInternet = false, + bool EmbedAssemblies = false, + string? ManifestPlaceholders = null, + string? CheckedBuild = null, + string? ApplicationJavaClass = null); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml new file mode 100644 index 00000000000..8c62546764a --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/PreserveLists/Trimmable.CoreCLR.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets index aa145ccb839..699b9baf1e3 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.LlvmIr.targets @@ -17,6 +17,13 @@ <_BeforeCompileToDalvik>$(_BeforeCompileToDalvik);_GetMonoPlatformJarPath + + + + <_GenerateJavaStubsDependsOnTargets> _SetLatestTargetFrameworkVersion; diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index a06b771e122..d30ab44e74b 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -1,15 +1,71 @@ - + - - + + <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">mono.MonoRuntimeProvider + + + + + + + %(Filename)%(Extension) + + + + + + + + <_ExtraTrimmerArgs>--typemap-entry-assembly $(_TypeMapAssemblyName) $(_ExtraTrimmerArgs) + + + + + + + <_CurrentAbi>%(_BuildTargetAbis.Identity) + <_CurrentRid Condition=" '$(_CurrentAbi)' == 'arm64-v8a' ">android-arm64 + <_CurrentRid Condition=" '$(_CurrentAbi)' == 'armeabi-v7a' ">android-arm + <_CurrentRid Condition=" '$(_CurrentAbi)' == 'x86_64' ">android-x64 + <_CurrentRid Condition=" '$(_CurrentAbi)' == 'x86' ">android-x86 + + + + <_CurrentLinkedTypeMapDlls Include="$(IntermediateOutputPath)$(_CurrentRid)/linked/_*.TypeMap.dll;$(IntermediateOutputPath)$(_CurrentRid)/linked/_Microsoft.Android.TypeMap*.dll" /> + + + <_BuildApkResolvedUserAssemblies Include="@(_CurrentLinkedTypeMapDlls)"> + $(_CurrentAbi) + $(_CurrentRid) + $(_CurrentAbi)/%(_CurrentLinkedTypeMapDlls.Filename)%(_CurrentLinkedTypeMapDlls.Extension) + $(_CurrentAbi)/ + + + + + <_CurrentTypeMapDlls Include="$(_TypeMapOutputDirectory)*.dll" /> + + + <_BuildApkResolvedUserAssemblies Include="@(_CurrentTypeMapDlls)"> + $(_CurrentAbi) + $(_CurrentRid) + $(_CurrentAbi)/%(_CurrentTypeMapDlls.Filename)%(_CurrentTypeMapDlls.Extension) + $(_CurrentAbi)/ + + - <_ResolvedAssemblies Include="@(_GeneratedTypeMapAssemblies)" /> + <_CurrentLinkedTypeMapDlls Remove="@(_CurrentLinkedTypeMapDlls)" /> + <_CurrentTypeMapDlls Remove="@(_CurrentTypeMapDlls)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index eabaffb2745..cc96c228bc7 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -2,6 +2,10 @@ Adds generated typemap assemblies to ILC inputs. --> + + <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">net.dot.jni.nativeaot.NativeAotRuntimeProvider + + 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 b056316d7db..d38aad44fb9 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 @@ -1,9 +1,8 @@ - + + @@ -12,94 +11,172 @@ <_TypeMapAssemblyName>_Microsoft.Android.TypeMaps - <_TypeMapOutputDirectory>$(IntermediateOutputPath)typemap\ - <_TypeMapJavaOutputDirectory>$(IntermediateOutputPath)typemap\java - <_PerAssemblyAcwMapDirectory>$(IntermediateOutputPath)acw-maps\ + <_TypeMapBaseOutputDir>$(IntermediateOutputPath) + <_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/')) + <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ + <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java - + + + - + Condition=" '$(_AndroidRuntime)' != 'CoreCLR' And '$(_AndroidRuntime)' != 'NativeAOT' " + BeforeTargets="Build"> + - - + + + + + <_TypeMapInputAssemblies Include="@(ReferencePath)" /> + <_TypeMapInputAssemblies Include="@(ResolvedAssemblies)" /> + <_TypeMapInputAssemblies Include="@(ResolvedFrameworkAssemblies)" /> + <_TypeMapInputAssemblies Include="$(IntermediateOutputPath)$(TargetFileName)" + Condition="Exists('$(IntermediateOutputPath)$(TargetFileName)')" /> + + TargetFrameworkVersion="$(TargetFrameworkVersion)" + ManifestTemplate="$(_AndroidManifestAbs)" + MergedAndroidManifestOutput="$(_TypeMapBaseOutputDir)AndroidManifest.xml" + PackageName="$(_AndroidPackage)" + ApplicationLabel="$(_ApplicationLabel)" + VersionCode="$(_AndroidVersionCode)" + VersionName="$(_AndroidVersionName)" + AndroidApiLevel="$(_AndroidApiLevel)" + SupportedOSPlatformVersion="$(SupportedOSPlatformVersion)" + RuntimeProviderJavaName="$(_TrimmableRuntimeProviderJavaName)" + Debug="$(AndroidIncludeDebugSymbols)" + NeedsInternet="$(AndroidNeedsInternetPermission)" + EmbedAssemblies="$(EmbedAssembliesIntoApk)" + ManifestPlaceholders="$(AndroidManifestPlaceholders)" + CheckedBuild="$(_AndroidCheckedBuild)" + ApplicationJavaClass="$(AndroidApplicationJavaClass)" + AcwMapOutputFile="$(IntermediateOutputPath)acw-map.txt" + ApplicationRegistrationOutputFile="$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java"> - + - + + + - - - - + + - <_PerAssemblyAcwMapFiles Remove="@(_PerAssemblyAcwMapFiles)" /> - <_PerAssemblyAcwMapFiles Include="$(_PerAssemblyAcwMapDirectory)*.txt" /> + <_TypeMapJavaFiles Include="$(_TypeMapJavaOutputDirectory)/**/*.java" /> - + - - - - - - - - + + + + + + + <_TypeMapFirstAbi Condition=" '$(AndroidSupportedAbis)' != '' ">$([System.String]::Copy('$(AndroidSupportedAbis)').Split(';')[0]) + <_TypeMapFirstAbi Condition=" '$(_TypeMapFirstAbi)' == '' ">arm64-v8a + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'arm64-v8a' ">android-arm64 + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'armeabi-v7a' ">android-arm + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'x86_64' ">android-x64 + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'x86' ">android-x86 + + + + + <_ResolvedAssemblies Include="$(_TypeMapOutputDirectory)*.dll"> + $(_TypeMapFirstAbi) + $(_TypeMapFirstRid) + $(_TypeMapFirstAbi)/%(Filename)%(Extension) + $(_TypeMapFirstAbi)/ + + + + + + + + + + + + + + + + + + + + + + <_TypeMapStubAbis Include="@(_BuildTargetAbis)" /> + + + + + - + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index bc2d9a7d4d4..c278679f4ee 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1427,6 +1427,15 @@ public static string XA4249 { } } + /// + /// Looks up a localized string similar to Manifest-referenced type '{0}' was not found in any scanned assembly. It may be a framework type.. + /// + public static string XA4250 { + get { + return ResourceManager.GetString("XA4250", resourceCulture); + } + } + /// /// Looks up a localized string similar to Native library '{0}' will not be bundled because it has an unsupported ABI. Move this file to a directory with a valid Android ABI name such as 'libs/armeabi-v7a/'.. /// diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index d1ee02dc051..ca75ec196de 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -1068,6 +1068,11 @@ To use a custom JDK path for a command line build, set the 'JavaSdkDirectory' MS Maven artifact specification '{0}' is invalid. The correct format is 'group_id:artifact_id:version'. The following are literal names and should not be translated: Maven, group_id, artifact_id {0} - A Maven artifact specification + + + Manifest-referenced type '{0}' was not found in any scanned assembly. It may be a framework type. + The following are literal names and should not be translated: Manifest, framework. +{0} - Java type name from AndroidManifest.xml Command '{0}' failed.\n{1} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs new file mode 100644 index 00000000000..47d16267cda --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateEmptyTypemapStub.cs @@ -0,0 +1,97 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using Microsoft.Android.Build.Tasks; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Xamarin.Android.Tasks; + +/// +/// Generates empty native typemap LLVM IR stub files (typemap.{abi}.ll) for the trimmable typemap path. +/// These are compiled by the native toolchain to provide the type_map and related symbols that libmonodroid.so expects. +/// +public class GenerateEmptyTypemapStub : AndroidTask +{ + public override string TaskPrefix => "GETS"; + + [Required] + public string OutputDirectory { get; set; } = ""; + + [Required] + public ITaskItem [] Abis { get; set; } = []; + + public bool Debug { get; set; } + + [Output] + public ITaskItem []? Sources { get; set; } + + public override bool RunTask () + { + Directory.CreateDirectory (OutputDirectory); + var sources = new List (); + + foreach (var abi in Abis) { + string abiName = abi.ItemSpec; + string stubPath = Path.Combine (OutputDirectory, $"typemap.{abiName}.ll"); + Files.CopyIfStringChanged (GenerateStubLlvmIr (abiName), stubPath); + var item = new TaskItem (stubPath); + item.SetMetadata ("abi", abiName); + sources.Add (item); + } + + Sources = sources.ToArray (); + return !Log.HasLoggedErrors; + } + + string GenerateStubLlvmIr (string abi) + { + var (triple, datalayout) = abi switch { + "arm64-v8a" => ("aarch64-unknown-linux-android21", "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128"), + "x86_64" => ("x86_64-unknown-linux-android21", "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"), + "armeabi-v7a" => ("armv7-unknown-linux-androideabi21", "e-m:e-p:32:32-Fi8-i64:64-v128:64:128-a:0:32-n32-S64"), + "x86" => ("i686-unknown-linux-android21", "e-m:e-p:32:32-p270:32:32-p271:32:32-p272:64:64-f64:32:64-f80:32-n8:16:32-S128"), + _ => throw new NotSupportedException ($"Unsupported ABI: {abi}"), + }; + + string header = $$""" +; ModuleID = 'typemap.{{abi}}.ll' +source_filename = "typemap.{{abi}}.ll" +target datalayout = "{{datalayout}}" +target triple = "{{triple}}" + +"""; + + if (Debug) { + return header + """ +%struct.TypeMap = type { i32, i32, ptr, ptr } +%struct.TypeMapManagedTypeInfo = type { i64, i32, i32 } +%struct.TypeMapAssembly = type { i64 } + +@type_map = dso_local constant %struct.TypeMap zeroinitializer, align 8 +@typemap_use_hashes = dso_local constant i8 1, align 1 +@type_map_managed_type_info = dso_local constant [0 x %struct.TypeMapManagedTypeInfo] zeroinitializer, align 8 +@type_map_unique_assemblies = dso_local constant [0 x %struct.TypeMapAssembly] zeroinitializer, align 8 +@type_map_assembly_names = dso_local constant [1 x i8] zeroinitializer, align 1 +@type_map_managed_type_names = dso_local constant [1 x i8] zeroinitializer, align 1 +@type_map_java_type_names = dso_local constant [1 x i8] zeroinitializer, align 1 +"""; + } + + return header + """ +@managed_to_java_map_module_count = dso_local constant i32 0, align 4 +@managed_to_java_map = dso_local constant [0 x i8] zeroinitializer, align 8 +@java_to_managed_map = dso_local constant [0 x i8] zeroinitializer, align 8 +@java_to_managed_hashes = dso_local constant [0 x i64] zeroinitializer, align 8 +@modules_map_data = dso_local constant [0 x i8] zeroinitializer, align 8 +@modules_duplicates_data = dso_local constant [0 x i8] zeroinitializer, align 8 +@java_type_count = dso_local constant i32 0, align 4 +@java_type_names = dso_local constant [1 x i8] zeroinitializer, align 1 +@java_type_names_size = dso_local constant i64 0, align 8 +@managed_type_names = dso_local constant [1 x i8] zeroinitializer, align 1 +@managed_assembly_names = dso_local constant [1 x i8] zeroinitializer, align 1 +"""; + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index a17945d8cee..7b01f6f08e5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -334,7 +334,21 @@ void GetRequiredTokens (string assemblyFilePath, out int android_runtime_jnienv_ } if (android_runtime_jnienv_class_token == -1 || jnienv_initialize_method_token == -1 || jnienv_registerjninatives_method_token == -1) { - throw new InvalidOperationException ($"Unable to find the required Android.Runtime.JNIEnvInit method tokens for {assemblyFilePath}"); + if (!TargetsCLR) { + throw new InvalidOperationException ($"Required JNIEnvInit tokens not found in '{assemblyFilePath}' (class={android_runtime_jnienv_class_token}, init={jnienv_initialize_method_token}, register={jnienv_registerjninatives_method_token})."); + } + + // In the trimmable typemap path (CoreCLR), some JNIEnvInit methods may be trimmed. + // Use token 0 for missing tokens — native code will skip them. + if (jnienv_registerjninatives_method_token == -1) { + jnienv_registerjninatives_method_token = 0; + } + if (jnienv_initialize_method_token == -1) { + jnienv_initialize_method_token = 0; + } + if (android_runtime_jnienv_class_token == -1) { + android_runtime_jnienv_class_token = 0; + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 4d621afd2a9..7667ddb2a2c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -1,11 +1,11 @@ -#nullable enable - using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection.Metadata; using System.Reflection.PortableExecutable; +using System.Text; +using System.Xml.Linq; using Microsoft.Android.Build.Tasks; using Microsoft.Android.Sdk.TrimmableTypeMap; using Microsoft.Build.Framework; @@ -15,177 +15,322 @@ namespace Xamarin.Android.Tasks; public class GenerateTrimmableTypeMap : AndroidTask { -public override string TaskPrefix => "GTT"; - -[Required] -public ITaskItem [] ResolvedAssemblies { get; set; } = []; -[Required] -public string OutputDirectory { get; set; } = ""; -[Required] -public string JavaSourceOutputDirectory { get; set; } = ""; -[Required] -public string AcwMapDirectory { get; set; } = ""; -[Required] -public string TargetFrameworkVersion { get; set; } = ""; -[Output] -public ITaskItem [] GeneratedAssemblies { get; set; } = []; -[Output] -public ITaskItem [] GeneratedJavaFiles { get; set; } = []; -[Output] -public ITaskItem []? PerAssemblyAcwMapFiles { get; set; } - -public override bool RunTask () -{ -var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion); -var assemblyPaths = ResolvedAssemblies.Select (i => i.ItemSpec).Distinct ().ToList (); -// TODO(#10792): populate with framework assembly names to skip JCW generation for pre-compiled framework types -var frameworkAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); - -Directory.CreateDirectory (OutputDirectory); -Directory.CreateDirectory (JavaSourceOutputDirectory); -Directory.CreateDirectory (AcwMapDirectory); - -var peReaders = new List (); -var assemblies = new List<(string Name, PEReader Reader)> (); -TrimmableTypeMapResult? result = null; -try { -foreach (var path in assemblyPaths) { -var peReader = new PEReader (File.OpenRead (path)); -peReaders.Add (peReader); -var mdReader = peReader.GetMetadataReader (); -assemblies.Add ((mdReader.GetString (mdReader.GetAssemblyDefinition ().Name), peReader)); -} + sealed class MSBuildTrimmableTypeMapLogger : ITrimmableTypeMapLogger + { + readonly TaskLoggingHelper log; -var generator = new TrimmableTypeMapGenerator (msg => Log.LogMessage (MessageImportance.Low, msg)); -result = generator.Execute (assemblies, systemRuntimeVersion, frameworkAssemblyNames); + public MSBuildTrimmableTypeMapLogger (TaskLoggingHelper log) + { + this.log = log ?? throw new ArgumentNullException (nameof (log)); + } -GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); -GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); -PerAssemblyAcwMapFiles = GeneratePerAssemblyAcwMaps (result.AllPeers); -} finally { -if (result is not null) { -foreach (var assembly in result.GeneratedAssemblies) { -assembly.Content.Dispose (); -} -} -foreach (var peReader in peReaders) { -peReader.Dispose (); -} -} + public void LogNoJavaPeerTypesFound () + { + log.LogMessage (MessageImportance.Low, "No Java peer types found, skipping typemap generation."); + } -return !Log.HasLoggedErrors; -} + public void LogJavaPeerScanInfo (int assemblyCount, int peerCount) + { + log.LogMessage (MessageImportance.Low, $"Scanned {assemblyCount} assemblies, found {peerCount} Java peer types."); + } -ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies, IReadOnlyList assemblyPaths) -{ -// Build a map from assembly name -> source path for timestamp comparison -var sourcePathByName = new Dictionary (StringComparer.Ordinal); -foreach (var path in assemblyPaths) { -var name = Path.GetFileNameWithoutExtension (path); -sourcePathByName [name] = path; -} + public void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) + { + log.LogMessage (MessageImportance.Low, $"Generating JCW files for {jcwPeerCount} types (filtered from {totalPeerCount} total)."); + } -var items = new List (); -bool anyRegenerated = false; + public void LogDeferredRegistrationTypesInfo (int typeCount) + { + log.LogMessage (MessageImportance.Low, $"Found {typeCount} Application/Instrumentation types for deferred registration."); + } -foreach (var assembly in assemblies) { -if (assembly.Name == "_Microsoft.Android.TypeMaps") { -continue; // Handle root assembly separately below -} + public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) + { + log.LogMessage (MessageImportance.Low, $" {assemblyName}: {typeCount} types"); + } -string outputPath = Path.Combine (OutputDirectory, assembly.Name + ".dll"); -// Extract the original assembly name from the typemap name (e.g., "_Foo.TypeMap" -> "Foo") -string originalName = assembly.Name; -if (originalName.StartsWith ("_", StringComparison.Ordinal) && originalName.EndsWith (".TypeMap", StringComparison.Ordinal)) { -originalName = originalName.Substring (1, originalName.Length - ".TypeMap".Length - 1); -} + public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) + { + log.LogMessage (MessageImportance.Low, $" Root: {assemblyReferenceCount} per-assembly refs"); + } -if (IsUpToDate (outputPath, originalName, sourcePathByName)) { -Log.LogDebugMessage ($" {assembly.Name}: up to date, skipping"); -} else { -Files.CopyIfStreamChanged (assembly.Content, outputPath); -anyRegenerated = true; -Log.LogDebugMessage ($" {assembly.Name}: written"); -} + public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) + { + log.LogMessage (MessageImportance.Low, $"Generated {assemblyCount} typemap assemblies."); + } -items.Add (new TaskItem (outputPath)); -} + public void LogGeneratedJcwFilesInfo (int sourceCount) + { + log.LogMessage (MessageImportance.Low, $"Generated {sourceCount} JCW Java source files."); + } -// Root assembly — regenerate if any per-assembly typemap changed -var rootAssembly = assemblies.FirstOrDefault (a => a.Name == "_Microsoft.Android.TypeMaps"); -if (rootAssembly is not null) { -string rootOutputPath = Path.Combine (OutputDirectory, rootAssembly.Name + ".dll"); -if (anyRegenerated || !File.Exists (rootOutputPath)) { -Files.CopyIfStreamChanged (rootAssembly.Content, rootOutputPath); -Log.LogDebugMessage ($" Root: written"); -} else { -Log.LogDebugMessage ($" Root: up to date, skipping"); -} -items.Add (new TaskItem (rootOutputPath)); -} + public void LogUnresolvedTypeWarning (string name) + { + log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, name); + } -return items.ToArray (); -} + public void LogRootingManifestReferencedTypeInfo (string name, string managedTypeName) + { + log.LogMessage (MessageImportance.Low, $"Rooting manifest-referenced type '{name}' ({managedTypeName}) as unconditional."); + } + } -static bool IsUpToDate (string outputPath, string assemblyName, Dictionary sourcePathByName) -{ -if (!File.Exists (outputPath)) { -return false; -} -if (!sourcePathByName.TryGetValue (assemblyName, out var sourcePath)) { -return false; -} -return File.GetLastWriteTimeUtc (outputPath) >= File.GetLastWriteTimeUtc (sourcePath); -} + public override string TaskPrefix => "GTT"; -ITaskItem [] WriteJavaSourcesToDisk (IReadOnlyList javaSources) -{ -var items = new List (); -foreach (var source in javaSources) { -string outputPath = Path.Combine (JavaSourceOutputDirectory, source.RelativePath); -string? dir = Path.GetDirectoryName (outputPath); -if (!string.IsNullOrEmpty (dir)) { -Directory.CreateDirectory (dir); -} -using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) { -sw.Write (source.Content); -sw.Flush (); -Files.CopyIfStreamChanged (sw.BaseStream, outputPath); -} -items.Add (new TaskItem (outputPath)); -} -return items.ToArray (); -} + [Required] + public ITaskItem [] ResolvedAssemblies { get; set; } = []; + [Required] + public string OutputDirectory { get; set; } = ""; + [Required] + public string JavaSourceOutputDirectory { get; set; } = ""; + [Required] + public string TargetFrameworkVersion { get; set; } = ""; -ITaskItem [] GeneratePerAssemblyAcwMaps (IReadOnlyList allPeers) -{ -var peersByAssembly = allPeers -.GroupBy (p => p.AssemblyName, StringComparer.Ordinal) -.OrderBy (g => g.Key, StringComparer.Ordinal); -var outputFiles = new List (); -foreach (var group in peersByAssembly) { -var peers = group.ToList (); -string outputFile = Path.Combine (AcwMapDirectory, $"acw-map.{group.Key}.txt"); -using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) { -AcwMapWriter.Write (sw, peers); -sw.Flush (); -Files.CopyIfStreamChanged (sw.BaseStream, outputFile); -} -var item = new TaskItem (outputFile); -item.SetMetadata ("AssemblyName", group.Key); -outputFiles.Add (item); -} -return outputFiles.ToArray (); -} + public string? AcwMapOutputFile { get; set; } -static Version ParseTargetFrameworkVersion (string tfv) -{ -if (tfv.Length > 0 && (tfv [0] == 'v' || tfv [0] == 'V')) { -tfv = tfv.Substring (1); -} -if (Version.TryParse (tfv, out var version)) { -return version; -} -throw new ArgumentException ($"Cannot parse TargetFrameworkVersion '{tfv}' as a Version."); -} + public string? ApplicationRegistrationOutputFile { get; set; } + + public string? ManifestTemplate { get; set; } + + public string? MergedAndroidManifestOutput { get; set; } + + public string? PackageName { get; set; } + public string? ApplicationLabel { get; set; } + public string? VersionCode { get; set; } + public string? VersionName { get; set; } + public string? AndroidApiLevel { get; set; } + public string? SupportedOSPlatformVersion { get; set; } + public string? RuntimeProviderJavaName { get; set; } + public bool Debug { get; set; } + public bool NeedsInternet { get; set; } + public bool EmbedAssemblies { get; set; } + public string? ManifestPlaceholders { get; set; } + public string? CheckedBuild { get; set; } + public string? ApplicationJavaClass { get; set; } + + [Output] + public ITaskItem [] GeneratedAssemblies { get; set; } = []; + [Output] + public ITaskItem [] GeneratedJavaFiles { get; set; } = []; + [Output] + public string[]? AdditionalProviderSources { get; set; } + + public override bool RunTask () + { + var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion); + var assemblyPaths = ResolvedAssemblies.Select (i => i.ItemSpec).Distinct ().ToList (); + // TODO(#10792): populate with framework assembly names to skip JCW generation for pre-compiled framework types + var frameworkAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); + + Directory.CreateDirectory (OutputDirectory); + Directory.CreateDirectory (JavaSourceOutputDirectory); + + var peReaders = new List (); + var assemblies = new List<(string Name, PEReader Reader)> (); + TrimmableTypeMapResult? result = null; + try { + foreach (var path in assemblyPaths) { + var peReader = new PEReader (File.OpenRead (path)); + peReaders.Add (peReader); + var mdReader = peReader.GetMetadataReader (); + assemblies.Add ((mdReader.GetString (mdReader.GetAssemblyDefinition ().Name), peReader)); + } + + ManifestConfig? manifestConfig = null; + if (!MergedAndroidManifestOutput.IsNullOrEmpty () && !PackageName.IsNullOrEmpty ()) { + manifestConfig = new ManifestConfig ( + PackageName: PackageName, + ApplicationLabel: ApplicationLabel, + VersionCode: VersionCode, + VersionName: VersionName, + AndroidApiLevel: AndroidApiLevel, + SupportedOSPlatformVersion: SupportedOSPlatformVersion, + RuntimeProviderJavaName: RuntimeProviderJavaName, + Debug: Debug, + NeedsInternet: NeedsInternet, + EmbedAssemblies: EmbedAssemblies, + ManifestPlaceholders: ManifestPlaceholders, + CheckedBuild: CheckedBuild, + ApplicationJavaClass: ApplicationJavaClass); + } + + var generator = new TrimmableTypeMapGenerator (new MSBuildTrimmableTypeMapLogger (Log)); + + XDocument? manifestTemplate = null; + if (!ManifestTemplate.IsNullOrEmpty () && File.Exists (ManifestTemplate)) { + manifestTemplate = XDocument.Load (ManifestTemplate); + } + + result = generator.Execute ( + assemblies, + systemRuntimeVersion, + frameworkAssemblyNames, + manifestConfig, + manifestTemplate); + + GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); + GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); + + // Write manifest to disk if generated + if (result.Manifest is not null && !MergedAndroidManifestOutput.IsNullOrEmpty ()) { + var manifestDir = Path.GetDirectoryName (MergedAndroidManifestOutput); + if (!manifestDir.IsNullOrEmpty ()) { + Directory.CreateDirectory (manifestDir); + } + using (var ms = new MemoryStream ()) { + result.Manifest.Document.Save (ms); + ms.Position = 0; + Files.CopyIfStreamChanged (ms, MergedAndroidManifestOutput); + } + AdditionalProviderSources = result.Manifest.AdditionalProviderSources; + } + + // Write merged acw-map.txt if requested + if (!AcwMapOutputFile.IsNullOrEmpty ()) { + var acwDirectory = Path.GetDirectoryName (AcwMapOutputFile); + if (!acwDirectory.IsNullOrEmpty ()) { + Directory.CreateDirectory (acwDirectory); + } + using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) { + AcwMapWriter.Write (sw, result.AllPeers); + sw.Flush (); + Files.CopyIfStreamChanged (sw.BaseStream, AcwMapOutputFile); + } + Log.LogDebugMessage ($"Wrote merged acw-map.txt with {result.AllPeers.Count} types to {AcwMapOutputFile}."); + } + + // Generate ApplicationRegistration.java with registerNatives calls for + // Application/Instrumentation types whose static initializers were skipped. + if (!ApplicationRegistrationOutputFile.IsNullOrEmpty ()) { + var appRegDir = Path.GetDirectoryName (ApplicationRegistrationOutputFile); + if (!appRegDir.IsNullOrEmpty ()) { + Directory.CreateDirectory (appRegDir); + } + Files.CopyIfStringChanged (GenerateApplicationRegistrationJava (result.ApplicationRegistrationTypes), ApplicationRegistrationOutputFile); + Log.LogDebugMessage ($"Generated ApplicationRegistration.java with {result.ApplicationRegistrationTypes.Count} deferred registration(s)."); + } + } finally { + if (result is not null) { + foreach (var assembly in result.GeneratedAssemblies) { + assembly.Content.Dispose (); + } + } + foreach (var peReader in peReaders) { + peReader.Dispose (); + } + } + + return !Log.HasLoggedErrors; + } + + ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies, IReadOnlyList assemblyPaths) + { + // Build a map from assembly name -> source path for timestamp comparison + var sourcePathByName = new Dictionary (StringComparer.Ordinal); + foreach (var path in assemblyPaths) { + var name = Path.GetFileNameWithoutExtension (path); + sourcePathByName [name] = path; + } + + var items = new List (); + bool anyRegenerated = false; + + foreach (var assembly in assemblies) { + if (assembly.Name == "_Microsoft.Android.TypeMaps") { + continue; // Handle root assembly separately below + } + + string outputPath = Path.Combine (OutputDirectory, assembly.Name + ".dll"); + // Extract the original assembly name from the typemap name (e.g., "_Foo.TypeMap" -> "Foo") + string originalName = assembly.Name; + if (originalName.StartsWith ("_", StringComparison.Ordinal) && originalName.EndsWith (".TypeMap", StringComparison.Ordinal)) { + originalName = originalName.Substring (1, originalName.Length - ".TypeMap".Length - 1); + } + + if (IsUpToDate (outputPath, originalName, sourcePathByName)) { + Log.LogDebugMessage ($" {assembly.Name}: up to date, skipping"); + } else { + Files.CopyIfStreamChanged (assembly.Content, outputPath); + anyRegenerated = true; + Log.LogDebugMessage ($" {assembly.Name}: written"); + } + + items.Add (new TaskItem (outputPath)); + } + + // Root assembly — regenerate if any per-assembly typemap changed + var rootAssembly = assemblies.FirstOrDefault (a => a.Name == "_Microsoft.Android.TypeMaps"); + if (rootAssembly is not null) { + string rootOutputPath = Path.Combine (OutputDirectory, rootAssembly.Name + ".dll"); + if (anyRegenerated || !File.Exists (rootOutputPath)) { + Files.CopyIfStreamChanged (rootAssembly.Content, rootOutputPath); + Log.LogDebugMessage ($" Root: written"); + } else { + Log.LogDebugMessage ($" Root: up to date, skipping"); + } + items.Add (new TaskItem (rootOutputPath)); + } + + return items.ToArray (); + } + + static bool IsUpToDate (string outputPath, string assemblyName, Dictionary sourcePathByName) + { + if (!File.Exists (outputPath)) { + return false; + } + if (!sourcePathByName.TryGetValue (assemblyName, out var sourcePath)) { + return false; + } + return File.GetLastWriteTimeUtc (outputPath) >= File.GetLastWriteTimeUtc (sourcePath); + } + + ITaskItem [] WriteJavaSourcesToDisk (IReadOnlyList javaSources) + { + var items = new List (); + foreach (var source in javaSources) { + string outputPath = Path.Combine (JavaSourceOutputDirectory, source.RelativePath); + string? dir = Path.GetDirectoryName (outputPath); + if (!string.IsNullOrEmpty (dir)) { + Directory.CreateDirectory (dir); + } + using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) { + sw.Write (source.Content); + sw.Flush (); + Files.CopyIfStreamChanged (sw.BaseStream, outputPath); + } + items.Add (new TaskItem (outputPath)); + } + return items.ToArray (); + } + + static Version ParseTargetFrameworkVersion (string tfv) + { + if (tfv.Length > 0 && (tfv [0] == 'v' || tfv [0] == 'V')) { + tfv = tfv.Substring (1); + } + if (Version.TryParse (tfv, out var version)) { + return version; + } + throw new ArgumentException ($"Cannot parse TargetFrameworkVersion '{tfv}' as a Version."); + } + + static string GenerateApplicationRegistrationJava (IReadOnlyList registrationTypes) + { + var sb = new StringBuilder (); + sb.AppendLine ("package net.dot.android;"); + sb.AppendLine (); + sb.AppendLine ("public class ApplicationRegistration {"); + sb.AppendLine (); + sb.AppendLine ("\tpublic static android.content.Context Context;"); + sb.AppendLine (); + sb.AppendLine ("\tpublic static void registerApplications ()"); + sb.AppendLine ("\t{"); + foreach (var javaClassName in registrationTypes) { + sb.AppendLine ($"\t\tmono.android.Runtime.registerNatives ({javaClassName}.class);"); + } + sb.AppendLine ("\t}"); + sb.AppendLine ("}"); + return sb.ToString (); + } } 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 25ab24fa9a1..f76d95f246b 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 @@ -40,7 +40,6 @@ public void Execute_InvalidTargetFrameworkVersion_Fails () ResolvedAssemblies = [], OutputDirectory = outputDir, JavaSourceOutputDirectory = javaDir, - AcwMapDirectory = Path.Combine (Root, path, "acw-maps"), TargetFrameworkVersion = "not-a-version", }; @@ -132,7 +131,6 @@ GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, ResolvedAssemblies = assemblies, OutputDirectory = outputDir, JavaSourceOutputDirectory = javaDir, - AcwMapDirectory = Path.Combine (outputDir, "..", "acw-maps"), TargetFrameworkVersion = tfv, }; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs index f3ebbd59126..3c61b9cc765 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ExportFieldTests.cs @@ -20,8 +20,8 @@ public void Scanner_DetectsExportFieldsWithCorrectProperties () var staticField = peer.JavaFields.First (f => f.FieldName == "STATIC_INSTANCE"); Assert.True (staticField.IsStatic); Assert.Equal ("GetInstance", staticField.InitializerMethodName); - // Reference type — mapped via JNI signature, not fallback to java.lang.Object - Assert.Equal ("java.lang.Object", staticField.JavaTypeName); + // Reference type — mapped via JNI signature to the actual Java type + Assert.Equal ("my.app.ExportFieldExample", staticField.JavaTypeName); var instanceField = peer.JavaFields.First (f => f.FieldName == "VALUE"); Assert.False (instanceField.IsStatic); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 5cf9e1cabe9..8d33d20deeb 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -23,7 +23,7 @@ static string TestFixtureAssemblyPath { } static readonly Lazy<(List peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => { - var scanner = new JavaPeerScanner (); + using var scanner = new JavaPeerScanner (); var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); var mdReader = peReader.GetMetadataReader (); var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs index 95afe0971f0..30d81218883 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/JcwJavaSourceGeneratorTests.cs @@ -138,16 +138,20 @@ public void Generate_AcwType_HasRegisterNativesStaticBlock () public void Generate_ApplicationType_SkipsRegisterNatives () { var java = GenerateFixture ("my/app/MyApplication"); - Assert.DoesNotContain ("registerNatives", java); Assert.DoesNotContain ("static {", java); + Assert.DoesNotContain ("if (getClass () == MyApplication.class) nctor_0 ();", java); + AssertContainsLine ("private static synchronized void __md_registerNatives ()\n", java); + AssertContainsLine ("mono.android.Runtime.registerNatives (MyApplication.class);\n", java); } [Fact] public void Generate_InstrumentationType_SkipsRegisterNatives () { var java = GenerateFixture ("my/app/MyInstrumentation"); - Assert.DoesNotContain ("registerNatives", java); Assert.DoesNotContain ("static {", java); + Assert.DoesNotContain ("if (getClass () == MyInstrumentation.class) nctor_0 ();", java); + AssertContainsLine ("private static synchronized void __md_registerNatives ()\n", java); + AssertContainsLine ("mono.android.Runtime.registerNatives (MyInstrumentation.class);\n", java); } } @@ -257,6 +261,61 @@ public void Generate_MarshalMethod_HasOverrideAndNativeDeclaration () AssertContainsLine ("public native void n_OnCreate_Landroid_os_Bundle_ (android.os.Bundle p0);\n", java); } + [Fact] + public void Generate_OverrideAcrossIntermediateMcwBase_HasMethodStub () + { + var java = GenerateFixture ("my/app/SelectableList"); + AssertContainsLine ("@Override\n", java); + AssertContainsLine ("public void setSelection (int p0)\n", java); + AssertContainsLine ("n_SetSelection_I (p0);\n", java); + AssertContainsLine ("public native void n_SetSelection_I (int p0);\n", java); + } + + [Fact] + public void Generate_OverrideAcrossGenericIntermediateMcwBase_HasMethodStub () + { + var java = GenerateFixture ("my/app/GenericSelectableList"); + AssertContainsLine ("@Override\n", java); + AssertContainsLine ("public void setSelection (int p0)\n", java); + AssertContainsLine ("n_SetSelection_I (p0);\n", java); + AssertContainsLine ("public native void n_SetSelection_I (int p0);\n", java); + } + + [Fact] + public void Generate_DeferredRegistrationType_LazilyRegistersBeforeNativeCallback () + { + var type = new JavaPeerInfo { + JavaName = "my/app/DeferredInstrumentation", + CompatJniName = "my/app/DeferredInstrumentation", + ManagedTypeName = "MyApp.DeferredInstrumentation", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "DeferredInstrumentation", + AssemblyName = "App", + BaseJavaName = "android/app/Instrumentation", + CannotRegisterInStaticConstructor = true, + MarshalMethods = new List { + new () { + JniName = "onCreate", + JniSignature = "(Landroid/os/Bundle;)V", + ManagedMethodName = "OnCreate", + NativeCallbackName = "n_OnCreate_Landroid_os_Bundle_", + Connector = "GetOnCreate_Landroid_os_Bundle_Handler", + }, + new () { + JniName = "onStart", + JniSignature = "()V", + ManagedMethodName = "OnStart", + NativeCallbackName = "n_OnStart", + Connector = "GetOnStartHandler", + }, + }, + }; + + var java = GenerateToString (type); + AssertContainsLine ("__md_registerNatives ();\n\t\tn_OnCreate_Landroid_os_Bundle_ (p0);\n", java); + AssertContainsLine ("__md_registerNatives ();\n\t\tn_OnStart ();\n", java); + } + } public class NestedType diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index 2653286fd20..a3e7904f654 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -1,6 +1,4 @@ -using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Xml.Linq; using Microsoft.Android.Sdk.TrimmableTypeMap; @@ -9,26 +7,11 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; -public class ManifestGeneratorTests : IDisposable +public class ManifestGeneratorTests { static readonly XNamespace AndroidNs = "http://schemas.android.com/apk/res/android"; static readonly XName AttName = AndroidNs + "name"; - string tempDir; - - public ManifestGeneratorTests () - { - tempDir = Path.Combine (Path.GetTempPath (), "ManifestGeneratorTests_" + Guid.NewGuid ().ToString ("N")); - Directory.CreateDirectory (tempDir); - } - - public void Dispose () - { - if (Directory.Exists (tempDir)) { - Directory.Delete (tempDir, recursive: true); - } - } - ManifestGenerator CreateDefaultGenerator () => new ManifestGenerator { PackageName = "com.example.app", ApplicationLabel = "My App", @@ -36,17 +19,10 @@ public void Dispose () VersionName = "1.0", MinSdkVersion = "21", TargetSdkVersion = "36", - AndroidRuntime = "coreclr", + RuntimeProviderJavaName = "mono.MonoRuntimeProvider", }; - string OutputPath => Path.Combine (tempDir, "AndroidManifest.xml"); - - string WriteTemplate (string xml) - { - var path = Path.Combine (tempDir, "template.xml"); - File.WriteAllText (path, xml); - return path; - } + static XDocument ParseTemplate (string xml) => XDocument.Parse (xml); static JavaPeerInfo CreatePeer ( string javaName, @@ -70,16 +46,12 @@ XDocument GenerateAndLoad ( ManifestGenerator gen, IReadOnlyList? peers = null, AssemblyManifestInfo? assemblyInfo = null, - string? templatePath = null) + XDocument? template = null) { peers ??= []; assemblyInfo ??= new AssemblyManifestInfo (); - XDocument? template = null; - if (!string.IsNullOrEmpty (templatePath) && File.Exists (templatePath)) { - template = XDocument.Load (templatePath); - } - gen.Generate (template, peers, assemblyInfo, OutputPath); - return XDocument.Load (OutputPath); + var (doc, _) = gen.Generate (template, peers, assemblyInfo); + return doc; } [Fact] @@ -281,6 +253,24 @@ public void Instrumentation_GoesToManifest () Assert.Null (appInstrumentation); } + [Fact] + public void Instrumentation_DefaultsTargetPackageToManifestPackage () + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/MyInstrumentation", new ComponentInfo { + Kind = ComponentKind.Instrumentation, + Properties = new Dictionary { + ["Label"] = "My Test", + }, + }); + + var doc = GenerateAndLoad (gen, [peer]); + var instrumentation = doc.Root?.Element ("instrumentation"); + + Assert.NotNull (instrumentation); + Assert.Equal ("com.example.app", (string?)instrumentation?.Attribute (AndroidNs + "targetPackage")); + } + [Fact] public void RuntimeProvider_Added () { @@ -305,7 +295,7 @@ public void RuntimeProvider_Added () public void TemplateManifest_Preserved () { var gen = CreateDefaultGenerator (); - var template = WriteTemplate ( + var template = ParseTemplate ( """ @@ -314,7 +304,7 @@ public void TemplateManifest_Preserved () """); - var doc = GenerateAndLoad (gen, templatePath: template); + var doc = GenerateAndLoad (gen, template: template); var app = doc.Root?.Element ("application"); Assert.Equal ("false", (string?)app?.Attribute (AndroidNs + "allowBackup")); @@ -384,7 +374,7 @@ public void ManifestPlaceholders_Replaced () var gen = CreateDefaultGenerator (); gen.ManifestPlaceholders = "myAuthority=com.example.auth;myKey=12345"; - var template = WriteTemplate ( + var template = ParseTemplate ( """ @@ -395,7 +385,7 @@ public void ManifestPlaceholders_Replaced () """); - var doc = GenerateAndLoad (gen, templatePath: template); + var doc = GenerateAndLoad (gen, template: template); var provider = doc.Root?.Element ("application")?.Elements ("provider") .FirstOrDefault (p => (string?)p.Attribute (AndroidNs + "name") == "com.example.MyProvider"); Assert.Equal ("com.example.auth", (string?)provider?.Attribute (AndroidNs + "authorities")); @@ -434,7 +424,7 @@ public void AbstractTypes_Skipped () public void ExistingType_NotDuplicated () { var gen = CreateDefaultGenerator (); - var template = WriteTemplate ( + var template = ParseTemplate ( """ @@ -449,7 +439,7 @@ public void ExistingType_NotDuplicated () Properties = new Dictionary { ["Label"] = "New Label" }, }); - var doc = GenerateAndLoad (gen, [peer], templatePath: template); + var doc = GenerateAndLoad (gen, [peer], template: template); var activities = doc.Root?.Element ("application")?.Elements ("activity") .Where (a => (string?)a.Attribute (AttName) == "com.example.app.ExistingActivity") .ToList (); @@ -556,7 +546,7 @@ public void AssemblyLevel_Application () public void AssemblyLevel_Deduplication () { var gen = CreateDefaultGenerator (); - var template = WriteTemplate ( + var template = ParseTemplate ( """ @@ -573,7 +563,7 @@ public void AssemblyLevel_Deduplication () info.UsesLibraries.Add (new UsesLibraryInfo { Name = "org.apache.http.legacy" }); info.MetaData.Add (new MetaDataInfo { Name = "existing.key", Value = "new_value" }); - var doc = GenerateAndLoad (gen, assemblyInfo: info, templatePath: template); + var doc = GenerateAndLoad (gen, assemblyInfo: info, template: template); var cameraPerms = doc.Root?.Elements ("uses-permission") .Where (p => (string?)p.Attribute (AttName) == "android.permission.CAMERA") @@ -616,4 +606,103 @@ public void ConfigChanges_EnumConversion () Assert.True (parts.Contains ("screenSize"), "configChanges should contain 'screenSize'"); Assert.Equal (3, parts.Length); } + + [Fact] + public void AssemblyLevel_SupportsGLTexture () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.SupportsGLTextures.Add (new SupportsGLTextureInfo { Name = "GL_OES_compressed_ETC1_RGB8_texture" }); + + var doc = GenerateAndLoad (gen, assemblyInfo: info); + var element = doc.Root?.Elements ("supports-gl-texture") + .FirstOrDefault (e => (string?)e.Attribute (AttName) == "GL_OES_compressed_ETC1_RGB8_texture"); + Assert.NotNull (element); + } + + [Fact] + public void AssemblyLevel_UsesPermissionFlags () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.UsesPermissions.Add (new UsesPermissionInfo { + Name = "android.permission.POST_NOTIFICATIONS", + UsesPermissionFlags = "neverForLocation", + }); + + var doc = GenerateAndLoad (gen, assemblyInfo: info); + var perm = doc.Root?.Elements ("uses-permission") + .FirstOrDefault (e => (string?)e.Attribute (AttName) == "android.permission.POST_NOTIFICATIONS"); + Assert.NotNull (perm); + Assert.Equal ("neverForLocation", (string?)perm?.Attribute (AndroidNs + "usesPermissionFlags")); + } + + [Fact] + public void AssemblyLevel_PermissionRoundIcon () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.Permissions.Add (new PermissionInfo { + Name = "com.example.MY_PERMISSION", + Properties = new Dictionary { + ["RoundIcon"] = "@mipmap/ic_launcher_round", + }, + }); + + var doc = GenerateAndLoad (gen, assemblyInfo: info); + var perm = doc.Root?.Elements ("permission") + .FirstOrDefault (e => (string?)e.Attribute (AttName) == "com.example.MY_PERMISSION"); + Assert.NotNull (perm); + Assert.Equal ("@mipmap/ic_launcher_round", (string?)perm?.Attribute (AndroidNs + "roundIcon")); + } + + [Fact] + public void AssemblyLevel_ApplicationBackupAgent () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.ApplicationProperties = new Dictionary { + ["BackupAgent"] = "MyApp.MyBackupAgent", + }; + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/app/MyBackupAgent", + CompatJniName = "com/example/app/MyBackupAgent", + ManagedTypeName = "MyApp.MyBackupAgent", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "MyBackupAgent", + AssemblyName = "TestApp", + }, + }; + + var doc = GenerateAndLoad (gen, peers: peers, assemblyInfo: info); + var app = doc.Root?.Element ("application"); + Assert.NotNull (app); + Assert.Equal ("com.example.app.MyBackupAgent", (string?)app?.Attribute (AndroidNs + "backupAgent")); + } + + [Fact] + public void AssemblyLevel_ApplicationManageSpaceActivity () + { + var gen = CreateDefaultGenerator (); + var info = new AssemblyManifestInfo (); + info.ApplicationProperties = new Dictionary { + ["ManageSpaceActivity"] = "MyApp.ManageActivity", + }; + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/app/ManageActivity", + CompatJniName = "com/example/app/ManageActivity", + ManagedTypeName = "MyApp.ManageActivity", + ManagedTypeNamespace = "MyApp", + ManagedTypeShortName = "ManageActivity", + AssemblyName = "TestApp", + }, + }; + + var doc = GenerateAndLoad (gen, peers: peers, assemblyInfo: info); + var app = doc.Root?.Element ("application"); + Assert.NotNull (app); + Assert.Equal ("com.example.app.ManageActivity", (string?)app?.Attribute (AndroidNs + "manageSpaceActivity")); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 7be68db2eb4..b05d5b76fde 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -12,6 +12,68 @@ public class TrimmableTypeMapGeneratorTests : FixtureTestBase { readonly List logMessages = new (); + sealed class TestTrimmableTypeMapLogger : ITrimmableTypeMapLogger + { + readonly List logMessages; + readonly List? warnings; + + public TestTrimmableTypeMapLogger (List logMessages, List? warnings = null) + { + this.logMessages = logMessages; + this.warnings = warnings; + } + + public void LogNoJavaPeerTypesFound () + { + logMessages.Add ("No Java peer types found, skipping typemap generation."); + } + + public void LogJavaPeerScanInfo (int assemblyCount, int peerCount) + { + logMessages.Add ($"Scanned {assemblyCount} assemblies, found {peerCount} Java peer types."); + } + + public void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) + { + logMessages.Add ($"Generating JCW files for {jcwPeerCount} types (filtered from {totalPeerCount} total)."); + } + + public void LogDeferredRegistrationTypesInfo (int typeCount) + { + logMessages.Add ($"Found {typeCount} Application/Instrumentation types for deferred registration."); + } + + public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) + { + logMessages.Add ($" {assemblyName}: {typeCount} types"); + } + + public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) + { + logMessages.Add ($" Root: {assemblyReferenceCount} per-assembly refs"); + } + + public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) + { + logMessages.Add ($"Generated {assemblyCount} typemap assemblies."); + } + + public void LogGeneratedJcwFilesInfo (int sourceCount) + { + logMessages.Add ($"Generated {sourceCount} JCW Java source files."); + } + + public void LogUnresolvedTypeWarning (string name) + { + warnings?.Add (name); + } + + public void LogRootingManifestReferencedTypeInfo (string name, string managedTypeName) + { + logMessages.Add ($"Rooting manifest-referenced type '{name}' ({managedTypeName}) as unconditional."); + } + } + [Fact] public void Execute_EmptyAssemblyList_ReturnsEmptyResults () { @@ -79,7 +141,249 @@ public void Execute_JavaSourcesHaveCorrectStructure () Assert.Contains ("class ", source.Content); } - TrimmableTypeMapGenerator CreateGenerator () => new (msg => logMessages.Add (msg)); + [Fact] + public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () + { + using var peReader = CreateTestFixturePEReader (); + var manifestTemplate = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var result = CreateGenerator ().Execute ( + new List<(string, PEReader)> { ("TestFixtures", peReader) }, + new Version (11, 0), + new HashSet (), + new ManifestConfig ( + PackageName: "my.app", + AndroidApiLevel: "35", + SupportedOSPlatformVersion: "21", + RuntimeProviderJavaName: "mono.MonoRuntimeProvider", + ManifestPlaceholders: "applicationId=my.app"), + manifestTemplate); + + var peer = result.AllPeers.First (p => p.ManagedTypeName == "MyApp.SimpleActivity"); + Assert.True (peer.IsUnconditional, "Relative manifest names should root correctly after placeholder substitution."); + } + + TrimmableTypeMapGenerator CreateGenerator () => new (new TestTrimmableTypeMapLogger (logMessages)); + + TrimmableTypeMapGenerator CreateGenerator (List warnings) => + new (new TestTrimmableTypeMapLogger (logMessages, warnings)); + + [Theory] + [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", "com.example.MyActivity")] + [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", ".MyActivity")] + [InlineData ("com/example/MyService", "com.example.MyService", "com.example", "service", "MyService")] + [InlineData ("crc64123456789abc/MyActivity", "my/app/MyActivity", "my.app", "activity", ".MyActivity")] + [InlineData ("com/example/Outer$Inner", "com.example.Outer$Inner", "com.example", "activity", "com.example.Outer$Inner")] + public void RootManifestReferencedTypes_RootsManifestReferencedTypes ( + string javaName, + string compatJniName, + string packageName, + string elementName, + string manifestName) + { + var peers = new List { + new JavaPeerInfo { + JavaName = javaName, CompatJniName = compatJniName, + ManagedTypeName = "MyApp.MyTarget", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyTarget", + AssemblyName = "MyApp", IsUnconditional = false, + }, + new JavaPeerInfo { + JavaName = "com/example/OtherType", CompatJniName = "com.example.OtherType", + ManagedTypeName = "MyApp.OtherType", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "OtherType", + AssemblyName = "MyApp", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse ($$""" + + + + <{{elementName}} android:name="{{manifestName}}" /> + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional, "The manifest-referenced type should be rooted as unconditional."); + Assert.False (peers [1].IsUnconditional, "Non-matching peers should remain conditional."); + Assert.Contains (logMessages, m => m.Contains ("Rooting manifest-referenced type")); + } + + [Fact] + public void RootManifestReferencedTypes_RootsApplicationAndInstrumentationTypes () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyApplication", CompatJniName = "com.example.MyApplication", + ManagedTypeName = "MyApp.MyApplication", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyApplication", + AssemblyName = "MyApp", IsUnconditional = false, + }, + new JavaPeerInfo { + JavaName = "com/example/MyInstrumentation", CompatJniName = "com.example.MyInstrumentation", + ManagedTypeName = "MyApp.MyInstrumentation", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyInstrumentation", + AssemblyName = "MyApp", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional, "Application type should be rooted from ."); + Assert.True (peers [1].IsUnconditional, "Instrumentation type should be rooted from ."); + Assert.True (peers [0].CannotRegisterInStaticConstructor, "Application type should defer Runtime.registerNatives()."); + Assert.True (peers [1].CannotRegisterInStaticConstructor, "Instrumentation type should defer Runtime.registerNatives()."); + } + + [Fact] + public void RootManifestReferencedTypes_PromotesCompatJniNameForManifestDeclaredApplication () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "crc64123456789abc/MyApplication", CompatJniName = "my/app/MyApplication", + ManagedTypeName = "MyApp.MyApplication", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyApplication", + AssemblyName = "MyApp", BaseJavaName = "android/app/Application", IsUnconditional = false, + }, + new JavaPeerInfo { + JavaName = "crc64123456789abc/MyDerivedApplication", CompatJniName = "my/app/MyDerivedApplication", + ManagedTypeName = "MyApp.MyDerivedApplication", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyDerivedApplication", + AssemblyName = "MyApp", BaseJavaName = "crc64123456789abc/MyApplication", IsUnconditional = false, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.Equal ("my/app/MyApplication", peers [0].JavaName); + Assert.Equal ("my/app/MyApplication", peers [1].BaseJavaName); + Assert.True (peers [0].CannotRegisterInStaticConstructor); + } + + [Fact] + public void Execute_DeferredRegistrationIncludesManagedBaseHierarchy () + { + using var peReader = CreateTestFixturePEReader (); + var result = CreateGenerator ().Execute ( + new List<(string, PEReader)> { ("TestFixtures", peReader) }, + new Version (11, 0), + new HashSet ()); + + Assert.Contains ("my.app.BaseApplication", result.ApplicationRegistrationTypes); + Assert.Contains ("my.app.MyApplication", result.ApplicationRegistrationTypes); + Assert.Contains ("my.app.BaseInstrumentation", result.ApplicationRegistrationTypes); + Assert.Contains ("my.app.IntermediateInstrumentation", result.ApplicationRegistrationTypes); + Assert.Contains ("my.app.MyInstrumentation", result.ApplicationRegistrationTypes); + + var baseInstrumentation = result.GeneratedJavaSources.Single (s => s.RelativePath == "my/app/BaseInstrumentation.java"); + var intermediateInstrumentation = result.GeneratedJavaSources.Single (s => s.RelativePath == "my/app/IntermediateInstrumentation.java"); + + Assert.DoesNotContain ("static {", baseInstrumentation.Content); + Assert.DoesNotContain ("static {", intermediateInstrumentation.Content); + Assert.Contains ("private static synchronized void __md_registerNatives ()", baseInstrumentation.Content); + Assert.Contains ("private static synchronized void __md_registerNatives ()", intermediateInstrumentation.Content); + Assert.DoesNotContain ("if (getClass () == BaseInstrumentation.class) nctor_0 ();", baseInstrumentation.Content); + Assert.DoesNotContain ("if (getClass () == IntermediateInstrumentation.class) nctor_0 ();", intermediateInstrumentation.Content); + } + + [Fact] + public void RootManifestReferencedTypes_WarnsForUnresolvedTypes () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", + ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var warnings = new List (); + var generator = CreateGenerator (warnings); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.Contains (warnings, w => w.Contains ("com.example.NonExistentService")); + } + + [Fact] + public void RootManifestReferencedTypes_SkipsAlreadyUnconditional () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", + ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", IsUnconditional = true, + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.True (peers [0].IsUnconditional); + Assert.DoesNotContain (logMessages, m => m.Contains ("Rooting manifest-referenced type")); + } + + [Fact] + public void RootManifestReferencedTypes_EmptyManifest_NoChanges () + { + var peers = new List { + new JavaPeerInfo { + JavaName = "com/example/MyActivity", CompatJniName = "com.example.MyActivity", + ManagedTypeName = "MyApp.MyActivity", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "MyActivity", + AssemblyName = "MyApp", + }, + }; + + var doc = System.Xml.Linq.XDocument.Parse (""" + + + + """); + + var generator = CreateGenerator (); + generator.RootManifestReferencedTypes (peers, doc); + + Assert.False (peers [0].IsUnconditional); + } static PEReader CreateTestFixturePEReader () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index d892b0161da..6bb225f1d06 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -84,6 +84,26 @@ public void Generate_ProxyType_HasCtorAndCreateInstance () Assert.Contains ("CreateInstance", methods); } + [Fact] + public void Generate_ProxyType_HasSelfAppliedJavaPeerProxyAttribute () + { + var peers = ScanFixtures (); + using var stream = GenerateAssembly (peers); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var objectProxy = reader.TypeDefinitions + .Select (h => (Handle: h, Type: reader.GetTypeDefinition (h))) + .First (t => reader.GetString (t.Type.Namespace) == "_TypeMap.Proxies" && + reader.GetString (t.Type.Name) == "Java_Lang_Object_Proxy"); + + var attrs = objectProxy.Type.GetCustomAttributes ().ToList (); + Assert.Single (attrs); + + var attr = reader.GetCustomAttribute (attrs [0]); + var ctor = reader.GetMethodDefinition ((MethodDefinitionHandle) attr.Constructor); + Assert.Equal (".ctor", reader.GetString (ctor.Name)); + } + [Fact] public void Generate_HasIgnoresAccessChecksToAttribute () { @@ -199,7 +219,21 @@ public void Generate_GenericType_ThrowsNotSupportedException () using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var typeNames = GetTypeRefNames (reader); + var generatedTypeNames = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Select (t => reader.GetString (t.Name)) + .ToList (); + var proxyMethods = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .Where (t => reader.GetString (t.Name) == "MyApp_Generic_GenericHolder_1_Proxy") + .SelectMany (t => t.GetMethods ()) + .Select (h => reader.GetString (reader.GetMethodDefinition (h).Name)) + .ToList (); Assert.Contains ("NotSupportedException", typeNames); + Assert.Contains ("MyApp_Generic_GenericHolder_1_Proxy", generatedTypeNames); + Assert.DoesNotContain (generatedTypeNames, name => name.Contains ('`')); + Assert.Contains ("RegisterNatives", proxyMethods); } [Fact] @@ -422,14 +456,15 @@ public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods () using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); - var memberNames = GetMemberRefNames (reader); - - // RegisterNatives is a method definition on the proxy type, not a member reference - var methodDefs = reader.MethodDefinitions - .Select (h => reader.GetMethodDefinition (h)) - .Select (m => reader.GetString (m.Name)) + var proxyMethods = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Namespace) == "_TypeMap.Proxies") + .Where (t => reader.GetString (t.Name) == "MyApp_MainActivity_Proxy") + .SelectMany (t => t.GetMethods ()) + .Select (h => reader.GetString (reader.GetMethodDefinition (h).Name)) .ToList (); - Assert.Contains ("RegisterNatives", methodDefs); + Assert.Contains ("RegisterNatives", proxyMethods); + Assert.Contains (proxyMethods, name => name.Contains ("_uco_")); } [Fact] @@ -545,6 +580,26 @@ public void Generate_AcwProxy_HasPrivateImplementationDetails () Assert.Contains ("", typeDefNames); } + [Fact] + public void Generate_AcwProxy_UsesAssemblyVisibleUtf8HelperTypes () + { + var peers = ScanFixtures (); + var acwPeer = peers.First (p => p.JavaName == "my/app/MainActivity"); + + using var stream = GenerateAssembly (new [] { acwPeer }, "Utf8VisibilityTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var utf8Helpers = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Where (t => reader.GetString (t.Name).StartsWith ("__utf8_", StringComparison.Ordinal)) + .ToList (); + + Assert.NotEmpty (utf8Helpers); + Assert.All (utf8Helpers, t => + Assert.Equal (TypeAttributes.NestedAssembly, t.Attributes & TypeAttributes.VisibilityMask)); + } + [Fact] public void Generate_MultipleAcwProxies_DeduplicatesUtf8Strings () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs index d1f9d04f7df..2910773d76a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/AssemblyAttributeScanningTests.cs @@ -66,4 +66,21 @@ public void MetaData_ConstructorArgAndNamedArg () Assert.NotNull (meta); Assert.Equal ("test-value", meta.Value); } + + [Fact] + public void UsesPermission_Flags () + { + var info = ScanAssemblyManifestInfo (); + var perm = info.UsesPermissions.FirstOrDefault (p => p.Name == "android.permission.POST_NOTIFICATIONS"); + Assert.NotNull (perm); + Assert.Equal ("neverForLocation", perm.UsesPermissionFlags); + } + + [Fact] + public void SupportsGLTexture_ConstructorArg () + { + var info = ScanAssemblyManifestInfo (); + var gl = info.SupportsGLTextures.FirstOrDefault (g => g.Name == "GL_OES_compressed_ETC1_RGB8_texture"); + Assert.NotNull (gl); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs index 13365887d5f..ec8ba3e7ed0 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -53,6 +53,19 @@ public void Scan_MarshalMethod_ConstructorsAndSpecialCases () FindFixtureByManagedName ("Android.Views.IOnClickListener").InvokerTypeName); } + [Theory] + [InlineData ("processView", "(Landroid/view/View;)V")] + [InlineData ("handleClick", "(Landroid/view/View;I)Z")] + [InlineData ("getViewName", "(Landroid/view/View;)Ljava/lang/String;")] + public void Scan_ExportMethod_ResolvesJavaBoundParameterTypes (string jniName, string expectedSig) + { + var method = FindFixtureByJavaName ("my/app/ExportWithJavaBoundParams") + .MarshalMethods.FirstOrDefault (m => m.JniName == jniName); + Assert.NotNull (method); + Assert.Equal (expectedSig, method.JniSignature); + Assert.Null (method.Connector); + } + [Theory] [InlineData ("android/app/Activity", "Android.App.Activity")] [InlineData ("my/app/SimpleActivity", "Android.App.Activity")] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index d76129379db..edc7df9683f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -2,6 +2,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; +using System.Text; +using Java.Interop.Tools.JavaCallableWrappers; using Xunit; namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; @@ -87,4 +90,28 @@ public void Scan_RegisterAttribute_DotFormat_NormalizedToSlashes () Assert.False (peer.DoNotGenerateAcw); Assert.True (peer.IsUnconditional, "Should be unconditional due to [Activity]"); } + + [Theory] + [InlineData ("MyApp.PlainActivitySubclass")] + [InlineData ("MyApp.UnregisteredClickListener")] + [InlineData ("MyApp.UnregisteredExporter")] + public void Scan_UnregisteredType_MatchesJavaNativeTypeManager (string managedName) + { + var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) + ?? throw new InvalidOperationException ("Cannot determine test assembly directory"); + var fixtureAssemblyPath = Path.Combine (testAssemblyDir, "TestFixtures.dll"); + var fixtureAssembly = Assembly.LoadFrom (fixtureAssemblyPath); + var fixtureType = fixtureAssembly.GetType (managedName); + if (fixtureType is null) { + throw new InvalidOperationException ($"Could not load fixture type '{managedName}' from '{fixtureAssemblyPath}'."); + } + + var assemblyName = fixtureType.Assembly.GetName ().Name + ?? throw new InvalidOperationException ($"Could not determine assembly name for '{managedName}'."); + var data = Encoding.UTF8.GetBytes ($"{fixtureType.Namespace}:{assemblyName}"); + var hash = Crc64Helper.Compute (data); + var expectedJavaName = $"crc64{BitConverter.ToString (hash).Replace ("-", "").ToLowerInvariant ()}/{fixtureType.Name}"; + + Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs index 54f7e0e133a..0213fd3ebb6 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs @@ -102,6 +102,24 @@ public void MultipleOverloads_PicksCorrectOne () Assert.DoesNotContain (nonCtorMethods, m => m.JniName == "process" && m.JniSignature == "()V"); } + [Fact] + public void OverrideAcrossIntermediateMcwBase_Detected () + { + var peer = FindFixtureByJavaName ("my/app/SelectableList"); + var setSelection = Assert.Single (peer.MarshalMethods, m => m.JniName == "setSelection"); + Assert.Equal ("(I)V", setSelection.JniSignature); + Assert.Equal ("GetSetSelection_IHandler", setSelection.Connector); + } + + [Fact] + public void OverrideAcrossGenericIntermediateMcwBase_Detected () + { + var peer = FindFixtureByJavaName ("my/app/GenericSelectableList"); + var setSelection = Assert.Single (peer.MarshalMethods, m => m.JniName == "setSelection"); + Assert.Equal ("(I)V", setSelection.JniSignature); + Assert.Equal ("GetSetSelection_IHandler", setSelection.Connector); + } + [Fact] public void EmptyConnector_OverrideStillDetected () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs index f86a255829d..8d10610f08a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/AssemblyAttributes.cs @@ -4,6 +4,8 @@ [assembly: UsesFeature ("android.hardware.camera.autofocus", Required = false)] [assembly: UsesFeature (GLESVersion = 0x00020000)] [assembly: UsesPermission ("android.permission.INTERNET")] +[assembly: UsesPermission ("android.permission.POST_NOTIFICATIONS", UsesPermissionFlags = "neverForLocation")] [assembly: UsesLibrary ("org.apache.http.legacy")] [assembly: UsesLibrary ("com.example.optional", false)] [assembly: MetaData ("com.example.key", Value = "test-value")] +[assembly: SupportsGLTexture ("GL_OES_compressed_ETC1_RGB8_texture")] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 1e19e314e32..332abe00a19 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -110,6 +110,7 @@ public UsesPermissionAttribute () { } public string? Name { get; set; } public int MaxSdkVersion { get; set; } + public string? UsesPermissionFlags { get; set; } } [AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)] @@ -145,6 +146,14 @@ public sealed class IntentFilterAttribute : Attribute public string [] Actions { get; } public string []? Categories { get; set; } } + + [AttributeUsage (AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class SupportsGLTextureAttribute : Attribute + { + public SupportsGLTextureAttribute (string name) => Name = name; + + public string Name { get; private set; } + } } namespace Android.Content diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index a15b12ef758..776ca77c494 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -49,6 +49,20 @@ public class Service : Java.Lang.Object { protected Service (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } } + + [Register ("android/app/Application", DoNotGenerateAcw = true)] + public class Application : Java.Lang.Object + { + public Application () { } + protected Application (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + [Register ("android/app/Instrumentation", DoNotGenerateAcw = true)] + public class Instrumentation : Java.Lang.Object + { + public Instrumentation () { } + protected Instrumentation (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } } namespace Android.App.Backup @@ -284,6 +298,23 @@ public class ExportExample : Java.Lang.Object public void MyExportedMethod () { } } + /// + /// Has [Export] methods with non-primitive Java-bound parameter types. + /// The JCW should resolve parameter types via [Register] instead of falling back to Object. + /// + [Register ("my/app/ExportWithJavaBoundParams")] + public class ExportWithJavaBoundParams : Java.Lang.Object + { + [Java.Interop.Export ("processView")] + public void ProcessView (Android.Views.View view) { } + + [Java.Interop.Export ("handleClick")] + public bool HandleClick (Android.Views.View view, int action) { return false; } + + [Java.Interop.Export ("getViewName")] + public string GetViewName (Android.Views.View view) { return ""; } + } + /// /// Has [Export] methods with different access modifiers. /// The JCW should respect the C# visibility for [Export] methods. @@ -300,8 +331,11 @@ public void PublicMethod () { } protected void ProtectedMethod () { } } + [Register ("my/app/BaseApplication")] + public abstract class BaseApplication : Android.App.Application { } + [Application (Name = "my.app.MyApplication", BackupAgent = typeof (MyBackupAgent), ManageSpaceActivity = typeof (MyManageSpaceActivity))] - public class MyApplication : Java.Lang.Object { } + public class MyApplication : BaseApplication { } /// /// Has [ExportField] methods that should produce Java field declarations. @@ -318,8 +352,14 @@ protected ExportFieldExample (IntPtr handle, JniHandleOwnership transfer) : base public string GetValue () => ""; } + [Register ("my/app/BaseInstrumentation")] + public abstract class BaseInstrumentation : Android.App.Instrumentation { } + + [Register ("my/app/IntermediateInstrumentation")] + public abstract class IntermediateInstrumentation : BaseInstrumentation { } + [Instrumentation (Name = "my.app.MyInstrumentation")] - public class MyInstrumentation : Java.Lang.Object { } + public class MyInstrumentation : IntermediateInstrumentation { } [Register ("my/app/MyBackupAgent")] public class MyBackupAgent : Android.App.Backup.BackupAgent @@ -734,6 +774,72 @@ protected OverloadDerived (IntPtr handle, JniHandleOwnership transfer) : base (h public override void Process (int value) { } } + /// + /// Declares a registered abstract method above an intermediate MCW base type. + /// Mirrors AdapterView.SetSelection(int) for AbsListView-derived test fixtures. + /// + [Register ("my/app/SelectionHost", DoNotGenerateAcw = true)] + public abstract class SelectionHost : Java.Lang.Object + { + protected SelectionHost (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("setSelection", "(I)V", "GetSetSelection_IHandler")] + public abstract void SetSelection (int position); + } + + /// + /// Intermediate MCW base that inherits the registered method without redeclaring it. + /// + [Register ("my/app/SelectionContainer", DoNotGenerateAcw = true)] + public abstract class SelectionContainer : SelectionHost + { + protected SelectionContainer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + /// + /// Generic base used to verify override discovery through a generic-instantiated base type. + /// Mirrors AdapterView<T> in the real Mono.Android hierarchy. + /// + [Register ("my/app/GenericSelectionHost", DoNotGenerateAcw = true)] + public abstract class GenericSelectionHost : Java.Lang.Object where T : class + { + protected GenericSelectionHost (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("setSelection", "(I)V", "GetSetSelection_IHandler")] + public abstract void SetSelection (int position); + } + + /// + /// Intermediate MCW base that closes the generic base. + /// + [Register ("my/app/GenericSelectionContainer", DoNotGenerateAcw = true)] + public abstract class GenericSelectionContainer : GenericSelectionHost + { + protected GenericSelectionContainer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + /// + /// Overrides a registered method declared above the first MCW base in the hierarchy. + /// + [Register ("my/app/SelectableList")] + public class SelectableList : SelectionContainer + { + protected SelectableList (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + public override void SetSelection (int position) { } + } + + /// + /// Overrides a registered method declared above a generic-instantiated MCW base. + /// + [Register ("my/app/GenericSelectableList")] + public class GenericSelectableList : GenericSelectionContainer + { + protected GenericSelectableList (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + public override void SetSelection (int position) { } + } + /// /// Has a ctor with unsigned primitive params to test JNI mapping. /// From b676200bc409858b9797498cb4de6ab731767f0f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 8 Apr 2026 21:04:20 +0200 Subject: [PATCH 2/4] Inline activation ctor in UCO wrappers - UCO constructors directly call activation ctor (no ActivateInstance indirection) - WithinNewObjectScope guard prevents double peer creation - No-op UCO for open generic type definitions - ControlFlowBuilder support in PEAssemblyBuilder - Remove TrimmableNativeRegistration wrapper and ActivateInstance Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/PEAssemblyBuilder.cs | 17 +- .../Generator/TypeMapAssemblyEmitter.cs | 180 ++++++++++++++---- 2 files changed, 157 insertions(+), 40 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 56ae75638b2..eec632ad3d7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -239,7 +239,7 @@ TypeDefinitionHandle GetOrCreateSizedType (int size) int typeMethodStart = Metadata.GetRowCount (TableIndex.MethodDef) + 1; var handle = Metadata.AddTypeDefinition ( - TypeAttributes.NestedPrivate | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass, + TypeAttributes.NestedAssembly | TypeAttributes.ExplicitLayout | TypeAttributes.Sealed | TypeAttributes.AnsiClass, default, Metadata.GetOrAddString ($"__utf8_{size}"), Metadata.AddTypeReference (SystemRuntimeRef, @@ -259,7 +259,7 @@ TypeDefinitionHandle GetOrCreateSizedType (int size) /// public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL) - => EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null); + => EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null, useBranches: false); /// /// Emits a method body and definition with optional local variable declarations. @@ -269,9 +269,14 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, /// and must write the full LOCAL_SIG blob (header 0x07, /// compressed count, then each variable type). /// + /// + /// If true, creates a so the emitted IL can use + /// , , + /// and . + /// public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL, - Action? encodeLocals) + Action? encodeLocals, bool useBranches = false) { _sigBlob.Clear (); encodeSig (new BlobEncoder (_sigBlob)); @@ -287,7 +292,11 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, } _codeBlob.Clear (); - var encoder = new InstructionEncoder (_codeBlob); + ControlFlowBuilder? cfb = null; + if (useBranches) { + cfb = new ControlFlowBuilder (); + } + var encoder = cfb != null ? new InstructionEncoder (_codeBlob, cfb) : new InstructionEncoder (_codeBlob); emitIL (encoder); while (ILBuilder.Count % 4 != 0) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 34bfd15a9b6..0f277287888 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -43,7 +43,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// [UnmanagedCallersOnly] /// public static void nctor_0_uco(IntPtr jnienv, IntPtr self) -/// => TrimmableNativeRegistration.ActivateInstance(self, typeof(Activity)); +/// => TrimmableTypeMap.ActivateInstance(self, typeof(Activity)); /// /// // Registers JNI native methods (ACWs only): /// public void RegisterNatives(JniType jniType) @@ -77,7 +77,6 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _systemTypeRef; TypeReferenceHandle _runtimeTypeHandleRef; TypeReferenceHandle _jniTypeRef; - TypeReferenceHandle _trimmableNativeRegistrationRef; TypeReferenceHandle _notSupportedExceptionRef; TypeReferenceHandle _runtimeHelpersRef; @@ -87,7 +86,8 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _notSupportedExceptionCtorRef; MemberReferenceHandle _jniObjectReferenceCtorRef; MemberReferenceHandle _jniEnvDeleteRefRef; - MemberReferenceHandle _activateInstanceRef; + MemberReferenceHandle _withinNewObjectScopeRef; + MemberReferenceHandle _throwIfOpenGenericActivationRef; MemberReferenceHandle _ucoAttrCtorRef; BlobHandle _ucoAttrBlobHandle; MemberReferenceHandle _typeMapAttrCtorRef2Arg; @@ -184,8 +184,6 @@ void EmitTypeReferences () metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle")); _jniTypeRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType")); - _trimmableNativeRegistrationRef = metadata.AddTypeReference (_pe.MonoAndroidRef, - metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("TrimmableNativeRegistration")); _notSupportedExceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, @@ -240,13 +238,22 @@ void EmitMemberReferences () p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); })); - _activateInstanceRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "ActivateInstance", - sig => sig.MethodSignature ().Parameters (2, + // JniEnvironment.get_WithinNewObjectScope() -> bool (static property) + // Used by UCO constructors to skip activation when the object is being + // created from managed code via JNIEnv.StartCreateInstance/NewObject. + _withinNewObjectScopeRef = _pe.AddMemberRef (_jniEnvironmentRef, "get_WithinNewObjectScope", + sig => sig.MethodSignature ().Parameters (0, + rt => rt.Type ().Boolean (), + p => { })); + + // TrimmableTypeMap.ThrowIfOpenGenericActivation() — static, internal + // Called by no-op UCO constructors for open generic types. + var trimmableTypeMapRef = _pe.Metadata.AddTypeReference (_pe.MonoAndroidRef, + _pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"), _pe.Metadata.GetOrAddString ("TrimmableTypeMap")); + _throwIfOpenGenericActivationRef = _pe.AddMemberRef (trimmableTypeMapRef, "ThrowIfOpenGenericActivation", + sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_systemTypeRef, false); - })); + p => { })); // JniNativeMethod..ctor(byte*, byte*, IntPtr) _jniNativeMethodCtorRef = _pe.AddMemberRef (_jniNativeMethodRef, ".ctor", @@ -347,6 +354,16 @@ void EmitTypeMapAssociationAttributeCtorRef () void EmitProxyType (JavaPeerProxyData proxy, Dictionary wrapperHandles) { + if (proxy.IsAcw) { + // RegisterNatives uses RVA-backed UTF-8 fields under . + // Materialize those helper types before adding the proxy TypeDef, otherwise the + // later RegisterNatives method can be attached to the helper type instead. + foreach (var reg in proxy.NativeRegistrations) { + _pe.GetOrAddUtf8Field (reg.JniMethodName); + _pe.GetOrAddUtf8Field (reg.JniSignature); + } + } + var metadata = _pe.Metadata; var typeDefHandle = metadata.AddTypeDefinition ( TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, @@ -361,7 +378,7 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }), encoder => { @@ -369,6 +386,7 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary), - // so the wrapper signature must include all parameters to match the ABI. // Only jnienv (arg 0) and self (arg 1) are used — the constructor parameters - // are not forwarded because ActivateInstance creates the managed peer using the + // are not forwarded because we create the managed peer using the // activation ctor (IntPtr, JniHandleOwnership), not the user-visible constructor. var jniParams = JniSignatureHelper.ParseParameterTypes (uco.JniSignature); int paramCount = 2 + jniParams.Count; - var handle = _pe.EmitBody (uco.WrapperName, - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - sig => sig.MethodSignature ().Parameters (paramCount, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().IntPtr (); // jnienv - p.AddParameter ().Type ().IntPtr (); // self - for (int j = 0; j < jniParams.Count; j++) - JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); - }), - encoder => { - encoder.LoadArgument (1); // self - encoder.OpCode (ILOpCode.Ldtoken); - encoder.Token (userTypeRef); - encoder.Call (_getTypeFromHandleRef); - encoder.Call (_activateInstanceRef); - encoder.OpCode (ILOpCode.Ret); + Action encodeSig = sig => sig.MethodSignature ().Parameters (paramCount, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); // jnienv + p.AddParameter ().Type ().IntPtr (); // self + for (int j = 0; j < jniParams.Count; j++) + JniSignatureHelper.EncodeClrType (p.AddParameter ().Type (), jniParams [j]); }); + // Open generic types can't be activated from Java — the type parameters are unknown. + // Emit a no-op UCO so the nctor native method is registered (avoiding UnsatisfiedLinkError). + // The WithinNewObjectScope guard will cause this to return immediately when called + // from JNIEnv.StartCreateInstance, and the legacy NewOpenGenericTypeThrows test + // expects NotSupportedException from the FinishCreateInstance path. + if (proxy.IsGenericDefinition) { + var noopHandle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + encoder => { + encoder.OpCode (ILOpCode.Ret); + }); + AddUnmanagedCallersOnlyAttribute (noopHandle); + return noopHandle; + } + + var activationCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ( + $"UCO constructor wrapper requires an activation ctor for '{uco.TargetType.ManagedTypeName}'"); + + MethodDefinitionHandle handle; + if (activationCtor.Style == ActivationCtorStyle.JavaInterop) { + var ctorRef = AddJavaInteropActivationCtorRef ( + activationCtor.IsOnLeafType ? targetRef : _pe.ResolveTypeRef (activationCtor.DeclaringType)); + + handle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + encoder => { + // Skip activation if the object is being created from managed code + // (e.g., JNIEnv.StartCreateInstance / JNIEnv.NewObject). + var skipLabel = encoder.DefineLabel (); + encoder.Call (_withinNewObjectScopeRef); + encoder.Branch (ILOpCode.Brtrue, skipLabel); + + if (!activationCtor.IsOnLeafType) { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (targetRef); + } + + encoder.LoadLocalAddress (0); + encoder.LoadArgument (1); // self + encoder.Call (_jniObjectReferenceCtorRef); + + if (activationCtor.IsOnLeafType) { + encoder.LoadLocalAddress (0); + encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.OpCode (ILOpCode.Pop); + } else { + encoder.LoadLocalAddress (0); + encoder.LoadConstantI4 (1); // JniObjectReferenceOptions.Copy + encoder.Call (ctorRef); + } + + encoder.MarkLabel (skipLabel); + encoder.OpCode (ILOpCode.Ret); + }, + EncodeJniObjectReferenceLocal, + useBranches: true); + } else { + var ctorRef = AddActivationCtorRef ( + activationCtor.IsOnLeafType ? targetRef : _pe.ResolveTypeRef (activationCtor.DeclaringType)); + + handle = _pe.EmitBody (uco.WrapperName, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + encodeSig, + encoder => { + // Skip activation if the object is being created from managed code + var skipLabel = encoder.DefineLabel (); + encoder.Call (_withinNewObjectScopeRef); + encoder.Branch (ILOpCode.Brtrue, skipLabel); + + if (activationCtor.IsOnLeafType) { + encoder.LoadArgument (1); // self + encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (ctorRef); + encoder.OpCode (ILOpCode.Pop); + } else { + encoder.OpCode (ILOpCode.Ldtoken); + encoder.Token (targetRef); + encoder.Call (_getTypeFromHandleRef); + encoder.Call (_getUninitializedObjectRef); + encoder.OpCode (ILOpCode.Castclass); + encoder.Token (targetRef); + + encoder.LoadArgument (1); // self + encoder.LoadConstantI4 (0); // JniHandleOwnership.DoNotTransfer + encoder.Call (ctorRef); + } + + encoder.MarkLabel (skipLabel); + encoder.OpCode (ILOpCode.Ret); + }, + encodeLocals: null, + useBranches: true); + } + AddUnmanagedCallersOnlyAttribute (handle); return handle; } From d5aefaabb2d08959e6d675f833029bab7099b104 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 8 Apr 2026 21:04:37 +0200 Subject: [PATCH 3/4] Runtime trimmable typemap support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TrimmableTypeMap.TryGetType: unwrap proxy to return real managed type - TryGetJniName: fallback chain (Register → compat → JavaNativeTypeManager) - GetProxyForPeer: JNI class hierarchy walk with caching - CreatePeer: IsAssignableFrom validation for JavaCast safety - TypeManager/JNIEnv: trimmable guards for legacy APIs - JNIEnvInit: JniRuntime.SetCurrent for background threads Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 6 + .../Android.Runtime/JNIEnvInit.cs | 2 + src/Mono.Android/Java.Interop/TypeManager.cs | 8 +- .../JavaMarshalValueManager.cs | 15 +- .../TrimmableTypeMap.cs | 219 ++++++++++++++---- .../TrimmableTypeMapTypeManager.cs | 4 +- 6 files changed, 199 insertions(+), 55 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 8b004855ba8..aa91db31463 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -442,6 +442,12 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr) internal static unsafe string? TypemapManagedToJava (Type type) { + if (RuntimeFeature.TrimmableTypeMap) { + // The trimmable typemap doesn't use the native typemap tables. + // Delegate to the managed TrimmableTypeMap instead. + return TrimmableTypeMap.Instance.TryGetJniName (type, out var jniName) ? jniName : null; + } + if (mvid_bytes == null) mvid_bytes = new byte[16]; diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 097c8e5d0ab..8bb98c4a993 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -110,6 +110,7 @@ internal static void InitializeJniRuntimeEarly (JnienvInitializeArgs args) internal static void InitializeJniRuntime (JniRuntime runtime, JnienvInitializeArgs args) { androidRuntime = runtime; + JniRuntime.SetCurrent (runtime); SetSynchronizationContext (); } @@ -159,6 +160,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) valueManager, args->jniAddNativeMethodRegistrationAttributePresent != 0 ); + JniRuntime.SetCurrent (androidRuntime); if (RuntimeFeature.TrimmableTypeMap) { TrimmableTypeMap.Initialize (); diff --git a/src/Mono.Android/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index 379558e3265..7152e911b3e 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -268,7 +268,13 @@ static Type monovm_typemap_java_to_managed (string java_type_name) return type; } - if (RuntimeFeature.IsMonoRuntime) { + if (RuntimeFeature.TrimmableTypeMap) { + // The trimmable typemap doesn't use the native typemap tables. + // Delegate to the managed TrimmableTypeMap instead. + if (!TrimmableTypeMap.Instance.TryGetType (class_name, out type)) { + return null; + } + } else if (RuntimeFeature.IsMonoRuntime) { type = monovm_typemap_java_to_managed (class_name); } else if (RuntimeFeature.IsCoreClrRuntime) { type = clr_typemap_java_to_managed (class_name); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index e1b7e975059..fa7aa786925 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -509,16 +509,11 @@ void ProcessContext (HandleContext* context) { if (RuntimeFeature.TrimmableTypeMap) { var typeMap = TrimmableTypeMap.Instance; - if (typeMap is not null && targetType is not null) { - var proxy = typeMap.GetProxyForManagedType (targetType); - if (proxy is not null) { - var peer = proxy.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer); - if (peer is not null) { - peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); - JniObjectReference.Dispose (ref reference, transfer); - return peer; - } - } + var peer = typeMap.CreatePeer (reference.Handle, JniHandleOwnership.DoNotTransfer, targetType); + if (peer is not null) { + peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); + JniObjectReference.Dispose (ref reference, transfer); + return peer; } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index f2c535dd8c7..a85233420ed 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -9,6 +9,7 @@ using System.Threading; using Android.Runtime; using Java.Interop; +using Java.Interop.Tools.TypeNameMappings; namespace Microsoft.Android.Runtime; @@ -28,6 +29,8 @@ class TrimmableTypeMap readonly IReadOnlyDictionary _typeMap; readonly ConcurrentDictionary _proxyCache = new (); + readonly ConcurrentDictionary _jniNameCache = new (); + readonly ConcurrentDictionary _peerProxyCache = new (StringComparer.Ordinal); TrimmableTypeMap () { @@ -67,7 +70,24 @@ unsafe void RegisterNatives () } internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type) - => _typeMap.TryGetValue (jniSimpleReference, out type); + { + if (!_typeMap.TryGetValue (jniSimpleReference, out var mappedType)) { + type = null; + return false; + } + + // External typemap entries point at the generated proxy for ACW-backed types. + // The JniTypeManager, however, must surface the real managed peer type so + // Java object activation and virtual dispatch resolve to the user's override + // instead of the bound Android base type. + var proxy = mappedType.GetCustomAttribute (inherit: false); + if (proxy is null) { + type = null; + return false; + } + type = proxy.TargetType; + return true; + } /// /// Finds the proxy for a managed type by resolving its JNI name (from [Register] or @@ -83,8 +103,10 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty return direct; } - // Resolve the JNI name from the managed type's attributes - if (!TryGetJniNameForType (type, out var jniName)) { + // Resolve the JNI name from attributes first, then fall back to the + // generated TypeMap entries for ACW/component types which don't carry + // [Register]/[JniTypeSignature] themselves. + if (!self.TryGetJniName (type, out var jniName)) { return null; } @@ -97,6 +119,104 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty }, this); } + internal bool TryGetJniName (Type type, [NotNullWhen (true)] out string? jniName) + { + if (_jniNameCache.TryGetValue (type, out jniName)) { + return jniName != null; + } + + if (TryGetJniNameForType (type, out jniName)) { + _jniNameCache [type] = jniName; + return true; + } + + if (TryGetCompatJniNameForAndroidComponent (type, out jniName)) { + _jniNameCache [type] = jniName; + return true; + } + + // Prefer the JavaNativeTypeManager calculation for user/application types, + // as it matches the ACW generation rules used during the build. + if (typeof (IJavaPeerable).IsAssignableFrom (type)) { + jniName = JavaNativeTypeManager.ToJniName (type); + if (!string.IsNullOrEmpty (jniName) && jniName != "java/lang/Object") { + _jniNameCache [type] = jniName; + return true; + } + } + + jniName = null; + return false; + } + + internal JavaPeerProxy? GetProxyForPeer (IntPtr handle, Type? targetType = null) + { + if (handle == IntPtr.Zero) { + return null; + } + + var selfRef = new JniObjectReference (handle); + var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); + + try { + while (jniClass.IsValid) { + var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); + if (className != null) { + if (_peerProxyCache.TryGetValue (className, out var cached)) { + if (cached != null && (targetType is null || targetType.IsAssignableFrom (cached.TargetType))) { + return cached; + } + } else if (_typeMap.TryGetValue (className, out var mappedType)) { + var proxy = mappedType.GetCustomAttribute (inherit: false); + _peerProxyCache [className] = proxy; + if (proxy != null && (targetType is null || targetType.IsAssignableFrom (proxy.TargetType))) { + return proxy; + } + } + } + + var super = JniEnvironment.Types.GetSuperclass (jniClass); + JniObjectReference.Dispose (ref jniClass); + jniClass = super; + } + } finally { + JniObjectReference.Dispose (ref jniClass); + } + + return null; + } + + internal IJavaPeerable? CreatePeer (IntPtr handle, JniHandleOwnership transfer, Type? targetType = null) + { + var proxy = GetProxyForPeer (handle, targetType); + if (proxy is null && targetType is not null) { + proxy = GetProxyForManagedType (targetType); + // Verify the Java object is actually assignable to the target Java type + // before creating the peer. Without this, we'd create invalid peers + // (e.g., IAppendableInvoker wrapping a java.lang.Integer). + if (proxy is not null && TryGetJniName (targetType, out var targetJniName)) { + var selfRef = new JniObjectReference (handle); + var objClass = JniEnvironment.Types.GetObjectClass (selfRef); + JniObjectReference targetClass; + try { + targetClass = JniEnvironment.Types.FindClass (targetJniName); + } catch { + JniObjectReference.Dispose (ref objClass); + proxy = null; + return null; + } + bool isAssignable = JniEnvironment.Types.IsAssignableFrom (objClass, targetClass); + JniObjectReference.Dispose (ref objClass); + JniObjectReference.Dispose (ref targetClass); + if (!isAssignable) { + proxy = null; + } + } + } + + return proxy?.CreateInstance (handle, transfer); + } + /// /// Resolves a managed type's JNI name from its /// (implemented by both [Register] and [JniTypeSignature]). @@ -113,18 +233,66 @@ internal static bool TryGetJniNameForType (Type type, [NotNullWhen (true)] out s return false; } + static bool TryGetCompatJniNameForAndroidComponent (Type type, [NotNullWhen (true)] out string? jniName) + { + if (!IsAndroidComponentType (type)) { + jniName = null; + return false; + } + + var (typeName, parentJniName, ns) = GetCompatTypeNameParts (type); + jniName = parentJniName is not null + ? $"{parentJniName}_{typeName}" + : ns.Length == 0 + ? typeName + : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; + return true; + } + + static bool IsAndroidComponentType (Type type) + { + return type.IsDefined (typeof (global::Android.App.ActivityAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.App.ApplicationAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.App.InstrumentationAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.App.ServiceAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.Content.BroadcastReceiverAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.Content.ContentProviderAttribute), inherit: false); + } + + static (string TypeName, string? ParentJniName, string Namespace) GetCompatTypeNameParts (Type type) + { + var nameParts = new List { SanitizeTypeName (type.Name) }; + var current = type; + string? parentJniName = null; + + while (current.DeclaringType is Type parentType) { + if (TryGetJniNameForType (parentType, out var explicitJniName) || + TryGetCompatJniNameForAndroidComponent (parentType, out explicitJniName)) { + parentJniName = explicitJniName; + break; + } + + nameParts.Add (SanitizeTypeName (parentType.Name)); + current = parentType; + } + + nameParts.Reverse (); + return (string.Join ("_", nameParts), parentJniName, current.Namespace ?? ""); + } + + static string SanitizeTypeName (string name) + { + var tick = name.IndexOf ('`'); + return (tick >= 0 ? name.Substring (0, tick) : name).Replace ('+', '_'); + } + /// /// Creates a peer instance using the proxy's CreateInstance method. /// Given a managed type, resolves the JNI name, finds the proxy, and calls CreateInstance. /// internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transfer) { - var proxy = GetProxyForManagedType (type); - if (proxy is null) { - return false; - } - - return proxy.CreateInstance (handle, transfer) != null; + return CreatePeer (handle, transfer, type) != null; } const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; @@ -147,39 +315,6 @@ internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transf return GetProxyForManagedType (type)?.GetContainerFactory (); } - /// - /// Creates a managed peer instance for a Java object being constructed. - /// Called from generated UCO constructor wrappers (nctor_*_uco). - /// - internal static void ActivateInstance (IntPtr self, Type targetType) - { - var instance = s_instance; - if (instance is null) { - throw new InvalidOperationException ("TrimmableTypeMap has not been initialized."); - } - - // Look up the proxy via JNI class name → TypeMap dictionary. - // We can't use targetType.GetCustomAttribute() because the - // self-application attribute is on the proxy type, not the target type. - var selfRef = new JniObjectReference (self); - var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); - var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); - JniObjectReference.Dispose (ref jniClass); - - if (className is null || !instance._typeMap.TryGetValue (className, out var proxyType)) { - throw new InvalidOperationException ( - $"Failed to create peer for type '{targetType.FullName}' (jniClass='{className}'). " + - "Ensure the type has a generated proxy in the TypeMap assembly."); - } - - var proxy = proxyType.GetCustomAttribute (inherit: false); - if (proxy is null || proxy.CreateInstance (self, JniHandleOwnership.DoNotTransfer) is null) { - throw new InvalidOperationException ( - $"Failed to create peer for type '{targetType.FullName}'. " + - "Ensure the type has a generated proxy in the TypeMap assembly."); - } - } - [UnmanagedCallersOnly] static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 220be6aae78..c77b4219b85 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -32,7 +32,7 @@ protected override IEnumerable GetSimpleReferences (Type type) yield return r; } - if (TrimmableTypeMap.TryGetJniNameForType (type, out var jniName)) { + if (TrimmableTypeMap.Instance.TryGetJniName (type, out var jniName)) { yield return jniName; yield break; } @@ -40,7 +40,7 @@ protected override IEnumerable GetSimpleReferences (Type type) // Walk the base type chain for managed-only subclasses (e.g., JavaProxyThrowable // extends Java.Lang.Error but has no [Register] attribute itself). for (var baseType = type.BaseType; baseType is not null; baseType = baseType.BaseType) { - if (TrimmableTypeMap.TryGetJniNameForType (baseType, out var baseJniName)) { + if (TrimmableTypeMap.Instance.TryGetJniName (baseType, out var baseJniName)) { yield return baseJniName; yield break; } From a43745527fb3de4f9cbadab3f62f741c61153986 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 8 Apr 2026 21:04:54 +0200 Subject: [PATCH 4/4] Add trimmable typemap test plumbing and CI lane Use _AndroidTypeMapImplementation=trimmable directly instead of a MonoAndroidTypeMapFlavor wrapper property. This is consistent with the actual build system property used by the typemap targets. - _AndroidTypeMapImplementation=trimmable in CI yaml and test csproj - UseMonoRuntime=false set when _AndroidTypeMapImplementation=trimmable - TestsFlavor=CoreCLRTrimmable for result file naming - ExcludeCategories: NativeTypeMap, TrimmableIgnore, SSL - ExcludedTestNames for Java.Interop-Tests JavaObject-based fixtures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../yaml-templates/stage-package-tests.yaml | 12 +++++++++++- .../Java.Interop/JavaObjectExtensionsTests.cs | 2 +- .../Mono.Android-Tests/Java.Interop/JnienvTest.cs | 4 ++-- .../Mono.Android-Tests/Java.Lang/ObjectTest.cs | 2 ++ .../Mono.Android.NET-Tests.csproj | 6 ++++++ .../NUnitInstrumentation.cs | 14 ++++++++++++++ 6 files changed, 36 insertions(+), 4 deletions(-) diff --git a/build-tools/automation/yaml-templates/stage-package-tests.yaml b/build-tools/automation/yaml-templates/stage-package-tests.yaml index 869ac45cee1..0133dd5d0cd 100644 --- a/build-tools/automation/yaml-templates/stage-package-tests.yaml +++ b/build-tools/automation/yaml-templates/stage-package-tests.yaml @@ -199,10 +199,20 @@ stages: testName: Mono.Android.NET_Tests-CoreCLR project: tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj testResultsFiles: TestResult-Mono.Android.NET_Tests-$(XA.Build.Configuration)CoreCLR.xml - extraBuildArgs: -p:TestsFlavor=CoreCLR -p:UseMonoRuntime=false + extraBuildArgs: -p:_AndroidTypeMapImplementation=llvm-ir 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/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..1ef98e558e7 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,7 +15,7 @@ namespace Java.InteropTests { [TestFixture] public class JavaObjectExtensionsTests { - [Test] + [Test, Category ("TrimmableIgnore")] public void JavaCast_BaseToGenericWrapper () { using (var list = new JavaList (new[]{ 1, 2, 3 })) 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..be5347a780a 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,7 +121,7 @@ public void InvokingNullInstanceDoesNotCrashDalvik () } } - [Test] + [Test, Category ("TrimmableIgnore")] public void NewOpenGenericTypeThrows () { try { @@ -301,7 +301,7 @@ public void ActivatedDirectObjectSubclassesShouldBeRegistered () } } - [Test] + [Test, Category ("TrimmableIgnore")] public void ActivatedDirectThrowableSubclassesShouldBeRegistered () { if (Build.VERSION.SdkInt <= BuildVersionCodes.GingerbreadMr1) 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..a5f61e6c54c 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 @@ -72,9 +72,11 @@ public void JnienvCreateInstance_RegistersMultipleInstances () var intermediate = CreateInstance_OverrideAbsListView_Adapter.Intermediate; var registered = Java.Lang.Object.GetObject(adapter.Handle, JniHandleOwnership.DoNotTransfer); + var asBase = Java.Lang.Object.GetObject(adapter.Handle, JniHandleOwnership.DoNotTransfer); Assert.AreNotSame (adapter, intermediate); Assert.AreSame (adapter, registered); + Assert.AreSame (adapter, asBase); } } 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 ae33bef1571..31011e1bd97 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 @@ -37,6 +37,12 @@ $(ExcludeCategories):InetAccess:NetworkInterfaces + + false + CoreCLRTrimmable + $(ExcludeCategories):NativeTypeMap:TrimmableIgnore:SSL + + 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..867dab2ffb9 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,20 @@ protected override string LogTag protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer) { + if (Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { + // Java.Interop-Tests fixtures that use JavaObject types (not Java.Lang.Object) + // don't have JCW Java classes in the trimmable APK, and method remapping + // tests require Java-side support not present in the trimmable path. + // Exclude these entire fixtures to prevent ClassNotFoundException crashes. + ExcludedTestNames = new [] { + "Java.InteropTests.JavaObjectTest", + "Java.InteropTests.InvokeVirtualFromConstructorTests", + "Java.InteropTests.JniPeerMembersTests", + "Java.InteropTests.JniTypeManagerTests", + "Java.InteropTests.JniValueMarshaler_object_ContractTests", + "Java.InteropTests.JavaExceptionTests.InnerExceptionIsNotAProxy", + }; + } } protected override IList GetTestAssemblies()