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..76f74839088 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 @@ -78,13 +78,6 @@ EnableNativeRuntimeLinking="$(_AndroidEnableNativeRuntimeLinking)"> - - - + + + + <_RewriteMarshalMethodsAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs index e8af07e4737..3d35b7483fb 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs @@ -1,6 +1,7 @@ #nullable enable -using System.Collections.Concurrent; +using System.Collections.Generic; using System.Linq; +using Java.Interop.Tools.Cecil; using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; using Xamarin.Android.Tools; @@ -13,15 +14,22 @@ namespace Xamarin.Android.Tasks; /// attributes, significantly improving startup performance and reducing runtime overhead for Android applications. /// /// -/// This task operates on the marshal method classifications produced by earlier pipeline stages and: -/// -/// 1. Retrieves marshal method classifications from the build pipeline state -/// 2. Parses environment files to determine exception transition behavior -/// 3. Rewrites assemblies to replace dynamic registration with static marshal methods -/// 4. Optionally builds managed lookup tables for runtime marshal method resolution -/// 5. Reports statistics on marshal method generation and any fallback to dynamic registration -/// -/// The rewriting process creates native callback wrappers for methods that have non-blittable +/// This task runs in the inner (per-RID) build after ILLink and _PostTrimmingPipeline but before +/// CreateReadyToRunImages or IlcCompile. It is fully self-contained: it creates its own +/// assembly resolver, scans assemblies for marshal method candidates, classifies them, and rewrites them +/// in a single pass. +/// +/// The rewriting process: +/// +/// 1. Derives the target architecture from the +/// 2. Creates an assembly resolver with ReadWrite+InMemory mode for Cecil +/// 3. Scans assemblies to discover marshal method candidates via +/// 4. Parses environment files to determine exception transition behavior +/// 5. Rewrites assemblies to replace dynamic registration with static marshal methods +/// 6. Optionally builds managed lookup tables for runtime marshal method resolution +/// 7. Reports statistics on marshal method generation and any fallback to dynamic registration +/// +/// The rewriting creates native callback wrappers for methods that have non-blittable /// parameters or return types, ensuring compatibility with the [UnmanagedCallersOnly] attribute /// while maintaining proper marshaling semantics. /// @@ -32,6 +40,13 @@ public class RewriteMarshalMethods : AndroidTask /// public override string TaskPrefix => "RMM"; + /// + /// Gets or sets the resolved assemblies to scan and rewrite. + /// These are the post-ILLink assemblies from ResolvedFileToPublish in the inner build. + /// + [Required] + public ITaskItem [] Assemblies { get; set; } = []; + /// /// Gets or sets whether to enable managed marshal methods lookup tables. /// When enabled, generates runtime lookup structures that allow dynamic resolution @@ -47,42 +62,53 @@ public class RewriteMarshalMethods : AndroidTask public ITaskItem [] Environments { get; set; } = []; /// - /// Gets or sets the intermediate output directory path. Required for retrieving - /// build state objects that contain marshal method classifications. + /// Gets or sets the runtime identifier for this inner build (e.g., "android-arm64"). + /// Used to derive the target architecture and ABI for assembly scanning and rewriting. /// [Required] - public string IntermediateOutputDirectory { get; set; } = ""; + public string RuntimeIdentifier { get; set; } = ""; /// /// Executes the marshal method rewriting task. This is the main entry point that - /// coordinates the entire assembly rewriting process across all target architectures. + /// coordinates the entire assembly rewriting process for the current target architecture. /// /// /// true if the task completed successfully; false if errors occurred during processing. /// /// /// The execution flow is: - /// - /// 1. Retrieve native code generation state from previous pipeline stages - /// 2. Parse environment files for configuration (e.g., broken exception transitions) - /// 3. For each target architecture: - /// - Rewrite assemblies to use marshal methods - /// - Add special case methods (e.g., TypeManager methods) - /// - Optionally build managed lookup tables - /// 4. Report statistics on marshal method generation - /// 5. Log warnings for methods that must fall back to dynamic registration - /// + /// + /// 1. Derive the target architecture and ABI from the RuntimeIdentifier + /// 2. Build a dictionary of assemblies and set Abi metadata on each item + /// 3. Create an assembly resolver with ReadWrite+InMemory mode + /// 4. Scan assemblies and classify marshal methods via MarshalMethodsCollection.FromAssemblies + /// 5. Parse environment files for configuration (e.g., broken exception transitions) + /// 6. Rewrite assemblies to use marshal methods + /// 7. Add special case methods (e.g., TypeManager methods) + /// 8. Optionally build managed lookup tables + /// 9. Report statistics on marshal method generation + /// 10. Log warnings for methods that must fall back to dynamic registration + /// /// The task handles the ordering dependency between special case methods and managed /// lookup tables - special cases must be added first so they appear in the lookup tables. /// public override bool RunTask () { - // Retrieve the stored NativeCodeGenState from the build pipeline - // This contains marshal method classifications from earlier stages - var nativeCodeGenStates = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal> ( - MonoAndroidHelper.GetProjectBuildSpecificTaskObjectKey (GenerateJavaStubs.NativeCodeGenStateRegisterTaskKey, WorkingDirectory, IntermediateOutputDirectory), - RegisteredTaskObjectLifetime.Build - ); + AndroidTargetArch arch = MonoAndroidHelper.RidToArch (RuntimeIdentifier); + string abi = MonoAndroidHelper.RidToAbi (RuntimeIdentifier); + + // Build the assemblies dictionary and set Abi metadata required by XAJavaTypeScanner + var assemblies = new Dictionary (Assemblies.Length); + foreach (var asm in Assemblies) { + asm.SetMetadata ("Abi", abi); + assemblies [asm.ItemSpec] = asm; + } + + // Create a resolver with ReadWrite+InMemory mode so Cecil can modify assemblies in place + using XAAssemblyResolver resolver = MonoAndroidHelper.MakeResolver (Log, useMarshalMethods: true, arch, assemblies); + + // Scan and classify marshal methods + MarshalMethodsCollection classifier = MarshalMethodsCollection.FromAssemblies (arch, [.. assemblies.Values], resolver, Log); // Parse environment files to determine configuration settings // We need to parse the environment files supplied by the user to see if they want to use broken exception transitions. This information is needed @@ -91,42 +117,33 @@ public override bool RunTask () var environmentParser = new EnvironmentFilesParser (); bool brokenExceptionTransitionsEnabled = environmentParser.AreBrokenExceptionTransitionsEnabled (Environments); - // Process each target architecture - foreach (var kvp in nativeCodeGenStates) { - NativeCodeGenState state = kvp.Value; - - if (state.Classifier is null) { - Log.LogError ("state.Classifier cannot be null if marshal methods are enabled"); - return false; - } - - // Handle the ordering dependency between special case methods and managed lookup tables - if (!EnableManagedMarshalMethodsLookup) { - // Standard path: rewrite first, then add special cases - RewriteMethods (state, brokenExceptionTransitionsEnabled); - state.Classifier.AddSpecialCaseMethods (); - } else { - // Managed lookup path: add special cases first so they appear in lookup tables - // We need to run `AddSpecialCaseMethods` before `RewriteMarshalMethods` so that we can see the special case - // methods (such as TypeManager.n_Activate_mm) when generating the managed lookup tables. - state.Classifier.AddSpecialCaseMethods (); - state.ManagedMarshalMethodsLookupInfo = new ManagedMarshalMethodsLookupInfo (Log); - RewriteMethods (state, brokenExceptionTransitionsEnabled); - } - - // Report statistics on marshal method generation - Log.LogDebugMessage ($"[{state.TargetArch}] Number of generated marshal methods: {state.Classifier.MarshalMethods.Count}"); - if (state.Classifier.DynamicallyRegisteredMarshalMethods.Count > 0) { - Log.LogWarning ($"[{state.TargetArch}] Number of methods in the project that will be registered dynamically: {state.Classifier.DynamicallyRegisteredMarshalMethods.Count}"); - } - - // Count and report methods that need blittable workaround wrappers - var wrappedCount = state.Classifier.MarshalMethods.Sum (m => m.Value.Count (m2 => m2.NeedsBlittableWorkaround)); - - if (wrappedCount > 0) { - // TODO: change to LogWarning once the generator can output code which requires no non-blittable wrappers - Log.LogDebugMessage ($"[{state.TargetArch}] Number of methods in the project that need marshal method wrappers: {wrappedCount}"); - } + // Handle the ordering dependency between special case methods and managed lookup tables + ManagedMarshalMethodsLookupInfo? managedLookupInfo = null; + if (!EnableManagedMarshalMethodsLookup) { + // Standard path: rewrite first, then add special cases + RewriteAssemblies (arch, classifier, resolver, managedLookupInfo, brokenExceptionTransitionsEnabled); + classifier.AddSpecialCaseMethods (); + } else { + // Managed lookup path: add special cases first so they appear in lookup tables + // We need to run `AddSpecialCaseMethods` before `RewriteMarshalMethods` so that we can see the special case + // methods (such as TypeManager.n_Activate_mm) when generating the managed lookup tables. + classifier.AddSpecialCaseMethods (); + managedLookupInfo = new ManagedMarshalMethodsLookupInfo (Log); + RewriteAssemblies (arch, classifier, resolver, managedLookupInfo, brokenExceptionTransitionsEnabled); + } + + // Report statistics on marshal method generation + Log.LogDebugMessage ($"[{arch}] Number of generated marshal methods: {classifier.MarshalMethods.Count}"); + if (classifier.DynamicallyRegisteredMarshalMethods.Count > 0) { + Log.LogWarning ($"[{arch}] Number of methods in the project that will be registered dynamically: {classifier.DynamicallyRegisteredMarshalMethods.Count}"); + } + + // Count and report methods that need blittable workaround wrappers + var wrappedCount = classifier.MarshalMethods.Sum (m => m.Value.Count (m2 => m2.NeedsBlittableWorkaround)); + + if (wrappedCount > 0) { + // TODO: change to LogWarning once the generator can output code which requires no non-blittable wrappers + Log.LogDebugMessage ($"[{arch}] Number of methods in the project that need marshal method wrappers: {wrappedCount}"); } return !Log.HasLoggedErrors; @@ -137,7 +154,10 @@ public override bool RunTask () /// Creates and executes the that handles /// the low-level assembly modification operations. /// - /// The native code generation state containing marshal method classifications and resolver. + /// The target Android architecture. + /// The marshal methods classifier containing method classifications. + /// The assembly resolver used to load and resolve assemblies. + /// Optional managed marshal methods lookup info for building lookup tables. /// /// Whether to generate code compatible with broken exception transitions. /// This affects how wrapper methods handle exceptions during JNI calls. @@ -150,13 +170,9 @@ public override bool RunTask () /// - Modifying assembly references and imports /// - Building managed lookup table entries /// - void RewriteMethods (NativeCodeGenState state, bool brokenExceptionTransitionsEnabled) + void RewriteAssemblies (AndroidTargetArch arch, MarshalMethodsCollection classifier, XAAssemblyResolver resolver, ManagedMarshalMethodsLookupInfo? managedLookupInfo, bool brokenExceptionTransitionsEnabled) { - if (state.Classifier == null) { - return; - } - - var rewriter = new MarshalMethodsAssemblyRewriter (Log, state.TargetArch, state.Classifier, state.Resolver, state.ManagedMarshalMethodsLookupInfo); + var rewriter = new MarshalMethodsAssemblyRewriter (Log, arch, classifier, resolver, managedLookupInfo); rewriter.Rewrite (brokenExceptionTransitionsEnabled); } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs index 7c6119f5347..ce2249b0945 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs @@ -68,11 +68,37 @@ static NativeCodeGenStateObject CreateNativeCodeGenState (AndroidTargetArch arch obj.MarshalMethods.Add (group.Key, methods); } + // Also include methods that were already rewritten in the inner build. + // After the inner build rewrites assemblies, the outer build's GenerateJavaStubs + // re-classifies them and puts already-rewritten methods (with _mm_wrapper + + // [UnmanagedCallersOnly]) into ConvertedMarshalMethods instead of MarshalMethods. + // We must include these so that GenerateNativeMarshalMethodSources produces + // correct native callback tables. + foreach (var group in state.Classifier.ConvertedMarshalMethods) { + if (!obj.MarshalMethods.TryGetValue (group.Key, out var methods)) { + methods = new List (group.Value.Count); + obj.MarshalMethods.Add (group.Key, methods); + } + + foreach (var method in group.Value) { + var entry = CreateEntry (method, state.ManagedMarshalMethodsLookupInfo); + methods.Add (entry); + } + } + return obj; } static MarshalMethodEntryObject CreateEntry (MarshalMethodEntry entry, ManagedMarshalMethodsLookupInfo? info) { + // For converted entries, use the wrapper method (ConvertedNativeCallback) for native + // callback tables — it has the correct MetadataToken and [UnmanagedCallersOnly] attribute. + // For regular entries, NativeCallback already returns the wrapper after the rewriter sets + // NativeCallbackWrapper. + MethodDefinition nativeCallbackMethod = entry is ConvertedMarshalMethodEntry converted + ? converted.ConvertedNativeCallback + : entry.NativeCallback; + var obj = new MarshalMethodEntryObject ( declaringType: CreateDeclaringType (entry.DeclaringType), implementedMethod: CreateMethod (entry.ImplementedMethod), @@ -80,12 +106,12 @@ static MarshalMethodEntryObject CreateEntry (MarshalMethodEntry entry, ManagedMa jniTypeName: entry.JniTypeName, jniMethodName: entry.JniMethodName, jniMethodSignature: entry.JniMethodSignature, - nativeCallback: CreateMethod (entry.NativeCallback), + nativeCallback: CreateMethod (nativeCallbackMethod), registeredMethod: CreateMethodBase (entry.RegisteredMethod) ); if (info is not null) { - (uint assemblyIndex, uint classIndex, uint methodIndex) = info.GetIndex (entry.NativeCallback); + (uint assemblyIndex, uint classIndex, uint methodIndex) = info.GetIndex (nativeCallbackMethod); obj.NativeCallback.AssemblyIndex = assemblyIndex; obj.NativeCallback.ClassIndex = classIndex;