From d8657666d547bf0bacefe58d4f1a12e0b34cc634 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 3 Apr 2026 11:31:16 -0700 Subject: [PATCH 1/3] [marshal methods] Move marshal method classification and rewriting to inner (per-RID) build Move marshal method classification, assembly rewriting, and LLVM IR generation into a new _RewriteMarshalMethodsInner target that runs AfterTargets=_PostTrimmingPipeline in the inner build. Previously, GenerateJavaStubs classified marshal methods and stored the result in NativeCodeGenState via BuildEngine4. RewriteMarshalMethods then consumed that state and mutated assemblies in-place, changing metadata tokens. Downstream outer-build tasks (GenerateTypeMappings, GenerateNativeMarshalMethodSources) could see stale token data from the pre-rewrite NativeCodeGenState. By moving classification and rewriting into the inner build, the outer build only ever sees already-rewritten assemblies. The typemap.xml files generated by _AfterILLinkAdditionalSteps (FindTypeMapObjectsStep) now contain correct post-rewrite tokens, so GenerateTypeMappings can always use the .xml path without special-casing. Key changes: - RewriteMarshalMethods is now self-contained: opens trimmed assemblies with Cecil, classifies marshal methods, rewrites in-place, and generates the marshal_methods..ll file - GenerateJavaStubs no longer classifies marshal methods (classifier is null) - GenerateNativeMarshalMethodSources skips .ll generation when marshal methods are enabled (inner build already produced the file) - GenerateTypeMappings always uses .typemap.xml files since tokens are now stable post-rewrite - MarshalMethodCecilAdapter gains CreateNativeCodeGenStateObjectFromClassifier for building the LLVM IR state without full NativeCodeGenState --- ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 33 +- .../Tasks/GenerateJavaStubs.cs | 10 +- .../GenerateNativeMarshalMethodSources.cs | 100 ++---- .../Tasks/GenerateTypeMappings.cs | 35 +-- .../Tasks/RewriteMarshalMethods.cs | 297 +++++++++++------- .../Utilities/MarshalMethodCecilAdapter.cs | 26 ++ 6 files changed, 285 insertions(+), 216 deletions(-) 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..72d48d3ddf2 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)"> - - - + + + + <_MarshalMethodsAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs index bf709baf4c6..ae00afa57e8 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs @@ -244,12 +244,10 @@ internal static Dictionary MaybeGetArchAssemblies (Dictionary return (false, null); } - MarshalMethodsCollection? marshalMethodsCollection = null; - - if (useMarshalMethods) - marshalMethodsCollection = MarshalMethodsCollection.FromAssemblies (arch, assemblies.Values.ToList (), resolver, Log); - - return (true, new NativeCodeGenState (arch, tdCache, resolver, allJavaTypes, javaTypesForJCW, marshalMethodsCollection)); + // Marshal method classification is now done in the inner build by + // RewriteMarshalMethods, so we never classify here. The NativeCodeGenState + // will have a null Classifier; downstream tasks must handle that. + return (true, new NativeCodeGenState (arch, tdCache, resolver, allJavaTypes, javaTypesForJCW, classifier: null)); } (List allJavaTypes, List javaTypesForJCW) ScanForJavaTypes (XAAssemblyResolver res, TypeDefinitionCache cache, Dictionary assemblies, Dictionary userAssemblies, bool useMarshalMethods) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeMarshalMethodSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeMarshalMethodSources.cs index a0134611a69..91fdf6961b1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeMarshalMethodSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeMarshalMethodSources.cs @@ -133,14 +133,14 @@ public override bool RunTask () NativeCodeGenStateCollection? nativeCodeGenStates = null; androidRuntime = MonoAndroidHelper.ParseAndroidRuntime (AndroidRuntime); - // Retrieve native code generation state only if we need it - if (EnableMarshalMethods || EnableNativeRuntimeLinking) { - // Retrieve the stored NativeCodeGenStateCollection (and remove it from the cache) - nativeCodeGenStates = BuildEngine4.UnregisterTaskObjectAssemblyLocal ( - MonoAndroidHelper.GetProjectBuildSpecificTaskObjectKey (GenerateJavaStubs.NativeCodeGenStateObjectRegisterTaskKey, WorkingDirectory, IntermediateOutputDirectory), - RegisteredTaskObjectLifetime.Build - ); - } + // Always unregister the NativeCodeGenStateCollection to clean up, regardless + // of whether we need the data. When marshal methods are enabled, the inner + // build has already generated the .ll files; when disabled, we generate + // empty/minimal .ll files. PInvoke preservation needs the state regardless. + nativeCodeGenStates = BuildEngine4.UnregisterTaskObjectAssemblyLocal ( + MonoAndroidHelper.GetProjectBuildSpecificTaskObjectKey (GenerateJavaStubs.NativeCodeGenStateObjectRegisterTaskKey, WorkingDirectory, IntermediateOutputDirectory), + RegisteredTaskObjectLifetime.Build + ); // Generate native code for each supported ABI foreach (var abi in SupportedAbis) @@ -183,14 +183,6 @@ void Generate (NativeCodeGenStateCollection? nativeCodeGenStates, string abi) var pinvokePreserveBaseAsmFilePath = EnableNativeRuntimeLinking ? Path.Combine (EnvironmentOutputDirectory, $"pinvoke_preserve.{targetAbi}") : null; var marshalMethodsLlFilePath = $"{marshalMethodsBaseAsmFilePath}.ll"; var pinvokePreserveLlFilePath = pinvokePreserveBaseAsmFilePath != null ? $"{pinvokePreserveBaseAsmFilePath}.ll" : null; - var (assemblyCount, uniqueAssemblyNames) = GetAssemblyCountAndUniqueNames (); - - // Create the appropriate runtime-specific generator - MarshalMethodsNativeAssemblyGenerator marshalMethodsAsmGen = androidRuntime switch { - Tasks.AndroidRuntime.MonoVM => MakeMonoGenerator (), - Tasks.AndroidRuntime.CoreCLR => MakeCoreCLRGenerator (), - _ => throw new NotSupportedException ($"Internal error: unsupported runtime type '{androidRuntime}'") - }; // Generate P/Invoke preservation code if native linking is enabled bool fileFullyWritten; @@ -212,6 +204,32 @@ void Generate (NativeCodeGenStateCollection? nativeCodeGenStates, string abi) } } + // When marshal methods are enabled, the inner build has already generated the + // marshal_methods..ll file — skip generation here. + if (EnableMarshalMethods) { + Log.LogDebugMessage ($"Marshal methods .ll file for '{targetAbi}' was generated by the inner build, skipping outer build generation."); + return; + } + + // Marshal methods are disabled — generate empty/minimal .ll files + var (assemblyCount, uniqueAssemblyNames) = GetAssemblyCountAndUniqueNames (); + + // Create the appropriate runtime-specific generator (disabled path — empty/minimal code) + MarshalMethodsNativeAssemblyGenerator marshalMethodsAsmGen = androidRuntime switch { + Tasks.AndroidRuntime.MonoVM => new MarshalMethodsNativeAssemblyGeneratorMonoVM ( + Log, + targetArch, + assemblyCount, + uniqueAssemblyNames + ), + Tasks.AndroidRuntime.CoreCLR => new MarshalMethodsNativeAssemblyGeneratorCoreCLR ( + Log, + targetArch, + uniqueAssemblyNames + ), + _ => throw new NotSupportedException ($"Internal error: unsupported runtime type '{androidRuntime}'") + }; + // Generate marshal methods code var marshalMethodsModule = marshalMethodsAsmGen.Construct (); using var marshalMethodsWriter = MemoryStreamPool.Shared.CreateStreamWriter (); @@ -228,56 +246,6 @@ void Generate (NativeCodeGenStateCollection? nativeCodeGenStates, string abi) MonoAndroidHelper.LogTextStreamContents (Log, $"Partial contents of file '{marshalMethodsLlFilePath}'", marshalMethodsWriter.BaseStream); } } - - /// - /// Creates a MonoVM-specific marshal methods generator. - /// Handles both enabled and disabled marshal methods scenarios. - /// - /// A configured MonoVM marshal methods generator. - MarshalMethodsNativeAssemblyGenerator MakeMonoGenerator () - { - if (EnableMarshalMethods) { - return new MarshalMethodsNativeAssemblyGeneratorMonoVM ( - Log, - assemblyCount, - uniqueAssemblyNames, - EnsureCodeGenState (nativeCodeGenStates, targetArch), - EnableManagedMarshalMethodsLookup - ); - } - - // Generate empty/minimal code when marshal methods are disabled - return new MarshalMethodsNativeAssemblyGeneratorMonoVM ( - Log, - targetArch, - assemblyCount, - uniqueAssemblyNames - ); - } - - /// - /// Creates a CoreCLR-specific marshal methods generator. - /// Handles both enabled and disabled marshal methods scenarios. - /// - /// A configured CoreCLR marshal methods generator. - MarshalMethodsNativeAssemblyGenerator MakeCoreCLRGenerator () - { - if (EnableMarshalMethods) { - return new MarshalMethodsNativeAssemblyGeneratorCoreCLR ( - Log, - uniqueAssemblyNames, - EnsureCodeGenState (nativeCodeGenStates, targetArch), - EnableManagedMarshalMethodsLookup - ); - } - - // Generate empty/minimal code when marshal methods are disabled - return new MarshalMethodsNativeAssemblyGeneratorCoreCLR ( - Log, - targetArch, - uniqueAssemblyNames - ); - } } /// diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs index 70a6a60f5a9..0db0d4ac7a0 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTypeMappings.cs @@ -53,8 +53,6 @@ public class GenerateTypeMappings : AndroidTask public override bool RunTask () { - var useMarshalMethods = !Debug && EnableMarshalMethods; - androidRuntime = MonoAndroidHelper.ParseAndroidRuntime (AndroidRuntime); if (androidRuntime == Xamarin.Android.Tasks.AndroidRuntime.NativeAOT) { // NativeAOT typemaps are generated in `Microsoft.Android.Sdk.ILLink.TypeMappingStep` @@ -62,14 +60,15 @@ public override bool RunTask () return !Log.HasLoggedErrors; } - // If using marshal methods, we cannot use the .typemap.xml files currently because - // the type token ids were changed by the marshal method rewriter after we wrote the .xml files. - if (!useMarshalMethods) - GenerateAllTypeMappings (); + // Always use the .typemap.xml file path. When marshal methods are enabled, + // the assemblies returned from the inner build are already rewritten, so the + // .typemap.xml files generated by _AfterILLinkAdditionalSteps have correct + // post-rewrite tokens. + GenerateAllTypeMappings (); - // Generate typemaps from the native code generator state (produced by the marshal method rewriter) - if (RunCheckedBuild || useMarshalMethods) - GenerateAllTypeMappingsFromNativeState (useMarshalMethods); + // In a checked build, also generate from native state for comparison + if (RunCheckedBuild) + GenerateAllTypeMappingsFromNativeState (); return !Log.HasLoggedErrors; } @@ -106,7 +105,7 @@ void GenerateTypeMap (AndroidTargetArch arch, List assemblies) AddOutputTypeMaps (tmg, state.TargetArch); } - void GenerateAllTypeMappingsFromNativeState (bool useMarshalMethods) + void GenerateAllTypeMappingsFromNativeState () { // Retrieve the stored NativeCodeGenState var nativeCodeGenStates = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal> ( @@ -114,23 +113,13 @@ void GenerateAllTypeMappingsFromNativeState (bool useMarshalMethods) RegisteredTaskObjectLifetime.Build ); - NativeCodeGenState? templateCodeGenState = null; - foreach (var kvp in nativeCodeGenStates) { NativeCodeGenState state = kvp.Value; - templateCodeGenState = state; - GenerateTypeMapFromNativeState (state, useMarshalMethods); + GenerateTypeMapFromNativeState (state); } - - if (templateCodeGenState is null) - throw new InvalidOperationException ($"Internal error: no native code generator state defined"); - - // Set for use by task later - if (useMarshalMethods) - NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent = templateCodeGenState.JniAddNativeMethodRegistrationAttributePresent; } - void GenerateTypeMapFromNativeState (NativeCodeGenState state, bool useMarshalMethods) + void GenerateTypeMapFromNativeState (NativeCodeGenState state) { if (androidRuntime == Xamarin.Android.Tasks.AndroidRuntime.NativeAOT) { // NativeAOT typemaps are generated in `Microsoft.Android.Sdk.ILLink.TypeMappingStep` @@ -144,7 +133,7 @@ void GenerateTypeMapFromNativeState (NativeCodeGenState state, bool useMarshalMe state = new NativeCodeGenState (state.TargetArch, new TypeDefinitionCache (), state.Resolver, [], [], state.Classifier); } - var tmg = new TypeMapGenerator (Log, new NativeCodeGenStateAdapter (state), androidRuntime) { RunCheckedBuild = RunCheckedBuild && !useMarshalMethods }; + var tmg = new TypeMapGenerator (Log, new NativeCodeGenStateAdapter (state), androidRuntime) { RunCheckedBuild = RunCheckedBuild }; tmg.Generate (Debug, SkipJniAddNativeMethodRegistrationAttributeScan, TypemapOutputDirectory); AddOutputTypeMaps (tmg, state.TargetArch); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs index e8af07e4737..e3351d31269 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs @@ -1,5 +1,7 @@ #nullable enable -using System.Collections.Concurrent; +using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; @@ -8,155 +10,222 @@ namespace Xamarin.Android.Tasks; /// -/// MSBuild task that rewrites .NET assemblies to use marshal methods instead of dynamic JNI registration. -/// This task modifies method implementations to use efficient native callbacks with [UnmanagedCallersOnly] -/// attributes, significantly improving startup performance and reducing runtime overhead for Android applications. +/// MSBuild task that classifies, rewrites, and generates LLVM IR for marshal methods in +/// the inner (per-RID) build. Runs after ILLink and _PostTrimmingPipeline on the trimmed +/// assemblies, and before ReadyToRun/crossgen2 so that R2R images are built from the +/// rewritten assemblies. +/// +/// The task performs the following steps: +/// +/// 1. Opens the trimmed assemblies with Cecil and classifies marshal methods via +/// +/// 2. Rewrites assemblies in-place: adds [UnmanagedCallersOnly] wrappers, removes +/// connector methods and callback delegate backing fields +/// 3. Generates the marshal_methods.{abi}.ll LLVM IR file into +/// (the outer build's intermediate dir) +/// +/// Because this runs in the inner build, the outer build sees already-rewritten assemblies +/// in @(ResolvedFileToPublish). Downstream consumers +/// (_AfterILLinkAdditionalSteps, GenerateTypeMappings) therefore work on +/// post-rewrite tokens, eliminating the token staleness problem. /// -/// -/// 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 -/// parameters or return types, ensuring compatibility with the [UnmanagedCallersOnly] attribute -/// while maintaining proper marshaling semantics. -/// public class RewriteMarshalMethods : AndroidTask { + public override string TaskPrefix => "RMM"; + /// - /// Gets the task prefix used for logging and error messages. + /// The trimmed assemblies to process (from @(ResolvedFileToPublish) filtered to .dll). /// - public override string TaskPrefix => "RMM"; + [Required] + public ITaskItem [] Assemblies { get; set; } = []; + + /// + /// The Android runtime type (MonoVM or CoreCLR). Determines which LLVM IR generator + /// to use for the marshal_methods.{abi}.ll file. + /// + [Required] + public string AndroidRuntime { get; set; } = ""; /// - /// Gets or sets whether to enable managed marshal methods lookup tables. - /// When enabled, generates runtime lookup structures that allow dynamic resolution - /// of marshal methods without string comparisons, improving runtime performance. + /// Whether to enable managed marshal methods lookup tables. /// public bool EnableManagedMarshalMethodsLookup { get; set; } /// - /// Gets or sets the environment files to parse for configuration settings. - /// These files may contain settings like XA_BROKEN_EXCEPTION_TRANSITIONS that - /// affect how marshal method wrappers are generated. + /// Whether marshal methods are enabled. Should always be true when this task + /// is invoked, but is kept as a property for clarity and consistency with the target + /// condition. + /// + public bool EnableMarshalMethods { get; set; } + + /// + /// Environment files to parse for configuration (e.g. XA_BROKEN_EXCEPTION_TRANSITIONS). /// public ITaskItem [] Environments { get; set; } = []; /// - /// Gets or sets the intermediate output directory path. Required for retrieving - /// build state objects that contain marshal method classifications. + /// Directory where the marshal_methods.{abi}.ll file is written. + /// Typically $(_OuterIntermediateOutputPath)android so the outer build can + /// find it via @(_MarshalMethodsAssemblySource). /// [Required] - public string IntermediateOutputDirectory { get; set; } = ""; + public string MarshalMethodsOutputDirectory { 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. + /// The RuntimeIdentifier for this inner build (e.g. android-arm64). + /// Converted to an ABI and target architecture internally. /// - /// - /// 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 - /// - /// 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. - /// + [Required] + public string RuntimeIdentifier { get; set; } = ""; + 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 - ); - - // 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 - // in order to properly generate wrapper methods in the marshal methods assembly rewriter. - // We don't care about those generated by us, since they won't contain the `XA_BROKEN_EXCEPTION_TRANSITIONS` variable we look for. + if (!EnableMarshalMethods) { + Log.LogDebugMessage ("Marshal methods are not enabled, skipping."); + return true; + } + + var androidRuntime = MonoAndroidHelper.ParseAndroidRuntime (AndroidRuntime); + + // Parse environment files for configuration (e.g. broken exception transitions) var environmentParser = new EnvironmentFilesParser (); bool brokenExceptionTransitionsEnabled = environmentParser.AreBrokenExceptionTransitionsEnabled (Environments); - // Process each target architecture - foreach (var kvp in nativeCodeGenStates) { - NativeCodeGenState state = kvp.Value; + string abi = MonoAndroidHelper.RidToAbi (RuntimeIdentifier); + var targetArch = MonoAndroidHelper.AbiToTargetArch (abi); + ProcessArchitecture (targetArch, abi, androidRuntime, brokenExceptionTransitionsEnabled); - if (state.Classifier is null) { - Log.LogError ("state.Classifier cannot be null if marshal methods are enabled"); - return false; - } + return !Log.HasLoggedErrors; + } - // 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); - } + void ProcessArchitecture (AndroidTargetArch targetArch, string abi, AndroidRuntime androidRuntime, bool brokenExceptionTransitionsEnabled) + { + // Step 1: Open assemblies with Cecil and classify marshal methods + // Build the dictionary keyed by assembly name that MakeResolver and FromAssemblies expect + var assemblyDict = new Dictionary (StringComparer.OrdinalIgnoreCase); + foreach (var item in Assemblies) { + var name = Path.GetFileNameWithoutExtension (item.ItemSpec); + assemblyDict [name] = item; + } + var assemblyItems = assemblyDict.Values.ToList (); - // 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}"); - } + XAAssemblyResolver resolver = MonoAndroidHelper.MakeResolver (Log, useMarshalMethods: true, targetArch, assemblyDict); - // Count and report methods that need blittable workaround wrappers - var wrappedCount = state.Classifier.MarshalMethods.Sum (m => m.Value.Count (m2 => m2.NeedsBlittableWorkaround)); + MarshalMethodsCollection classifier; + try { + classifier = MarshalMethodsCollection.FromAssemblies (targetArch, assemblyItems, resolver, Log); + } catch (Exception ex) { + Log.LogError ($"[{targetArch}] Failed to classify marshal methods: {ex.Message}"); + Log.LogDebugMessage (ex.ToString ()); + return; + } - 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}"); - } + // Step 2: Rewrite assemblies + if (!EnableManagedMarshalMethodsLookup) { + RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled); + classifier.AddSpecialCaseMethods (); + } else { + // When managed lookup is enabled, add special cases first so they + // appear in the lookup tables + classifier.AddSpecialCaseMethods (); + var lookupInfo = new ManagedMarshalMethodsLookupInfo (Log); + RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled, lookupInfo); } - return !Log.HasLoggedErrors; + ReportStatistics (targetArch, classifier); + + // Step 3: Build NativeCodeGenStateObject from Cecil state and generate .ll + var codeGenState = MarshalMethodCecilAdapter.CreateNativeCodeGenStateObjectFromClassifier (targetArch, classifier); + GenerateLlvmIr (targetArch, abi, androidRuntime, codeGenState); + + // Step 4: Dispose Cecil resolvers + resolver.Dispose (); } - /// - /// Performs the actual assembly rewriting for a specific target architecture. - /// Creates and executes the that handles - /// the low-level assembly modification operations. - /// - /// The native code generation state containing marshal method classifications and resolver. - /// - /// Whether to generate code compatible with broken exception transitions. - /// This affects how wrapper methods handle exceptions during JNI calls. - /// - /// - /// This method delegates the complex assembly rewriting logic to the specialized - /// class, which handles: - /// - Adding [UnmanagedCallersOnly] attributes to native callbacks - /// - Generating wrapper methods for non-blittable types - /// - Modifying assembly references and imports - /// - Building managed lookup table entries - /// - void RewriteMethods (NativeCodeGenState state, bool brokenExceptionTransitionsEnabled) + void RewriteAssemblies (AndroidTargetArch targetArch, MarshalMethodsCollection classifier, XAAssemblyResolver resolver, bool brokenExceptionTransitionsEnabled, ManagedMarshalMethodsLookupInfo? lookupInfo = null) { - if (state.Classifier == null) { - return; + var rewriter = new MarshalMethodsAssemblyRewriter (Log, targetArch, classifier, resolver, lookupInfo); + rewriter.Rewrite (brokenExceptionTransitionsEnabled); + } + + void ReportStatistics (AndroidTargetArch targetArch, MarshalMethodsCollection classifier) + { + Log.LogDebugMessage ($"[{targetArch}] Number of generated marshal methods: {classifier.MarshalMethods.Count}"); + + if (classifier.DynamicallyRegisteredMarshalMethods.Count > 0) { + Log.LogWarning ($"[{targetArch}] Number of methods in the project that will be registered dynamically: {classifier.DynamicallyRegisteredMarshalMethods.Count}"); } - var rewriter = new MarshalMethodsAssemblyRewriter (Log, state.TargetArch, state.Classifier, state.Resolver, state.ManagedMarshalMethodsLookupInfo); - rewriter.Rewrite (brokenExceptionTransitionsEnabled); + 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 ($"[{targetArch}] Number of methods in the project that need marshal method wrappers: {wrappedCount}"); + } + } + + void GenerateLlvmIr (AndroidTargetArch targetArch, string abi, AndroidRuntime androidRuntime, NativeCodeGenStateObject codeGenState) + { + var targetAbi = abi.ToLowerInvariant (); + var llFilePath = Path.Combine (MarshalMethodsOutputDirectory, $"marshal_methods.{targetAbi}.ll"); + var (assemblyCount, uniqueAssemblyNames) = GetAssemblyCountAndUniqueNames (); + + MarshalMethodsNativeAssemblyGenerator generator = androidRuntime switch { + Tasks.AndroidRuntime.MonoVM => new MarshalMethodsNativeAssemblyGeneratorMonoVM ( + Log, + assemblyCount, + uniqueAssemblyNames, + codeGenState, + EnableManagedMarshalMethodsLookup + ), + Tasks.AndroidRuntime.CoreCLR => new MarshalMethodsNativeAssemblyGeneratorCoreCLR ( + Log, + uniqueAssemblyNames, + codeGenState, + EnableManagedMarshalMethodsLookup + ), + _ => throw new NotSupportedException ($"Internal error: unsupported runtime type '{androidRuntime}'") + }; + + Directory.CreateDirectory (MarshalMethodsOutputDirectory); + + var module = generator.Construct (); + using var writer = MemoryStreamPool.Shared.CreateStreamWriter (); + bool fileFullyWritten = false; + + try { + generator.Generate (module, targetArch, writer, llFilePath); + writer.Flush (); + Files.CopyIfStreamChanged (writer.BaseStream, llFilePath); + fileFullyWritten = true; + Log.LogDebugMessage ($"[{targetArch}] Generated marshal methods LLVM IR: {llFilePath}"); + } finally { + if (!fileFullyWritten) { + MonoAndroidHelper.LogTextStreamContents (Log, $"Partial contents of file '{llFilePath}'", writer.BaseStream); + } + } + } + + (int assemblyCount, HashSet uniqueAssemblyNames) GetAssemblyCountAndUniqueNames () + { + var assemblyCount = 0; + var uniqueAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); + + foreach (var assembly in Assemblies) { + var culture = MonoAndroidHelper.GetAssemblyCulture (assembly); + var fileName = Path.GetFileName (assembly.ItemSpec); + string assemblyName; + + if (culture.IsNullOrEmpty ()) { + assemblyName = fileName; + } else { + assemblyName = $"{culture}/{fileName}"; + } + + if (uniqueAssemblyNames.Add (assemblyName)) { + assemblyCount++; + } + } + + return (assemblyCount, uniqueAssemblyNames); } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs index 7c6119f5347..1d50aeb7720 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs @@ -71,6 +71,32 @@ static NativeCodeGenStateObject CreateNativeCodeGenState (AndroidTargetArch arch return obj; } + /// + /// Creates a from a marshal methods classifier, + /// for use in the inner build where the full (with + /// JavaTypesForJCW, TypeCache, etc.) is not available. Only populates marshal methods + /// data needed for LLVM IR generation. + /// + public static NativeCodeGenStateObject CreateNativeCodeGenStateObjectFromClassifier (AndroidTargetArch arch, MarshalMethodsCollection classifier) + { + var obj = new NativeCodeGenStateObject { + TargetArch = arch, + }; + + foreach (var group in classifier.MarshalMethods) { + var methods = new List (group.Value.Count); + + foreach (var method in group.Value) { + var entry = CreateEntry (method, info: null); + methods.Add (entry); + } + + obj.MarshalMethods.Add (group.Key, methods); + } + + return obj; + } + static MarshalMethodEntryObject CreateEntry (MarshalMethodEntry entry, ManagedMarshalMethodsLookupInfo? info) { var obj = new MarshalMethodEntryObject ( From 096e4aefa86662f042ea25ad10c3e9ffdd7e34c2 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 3 Apr 2026 15:42:21 -0700 Subject: [PATCH 2/3] [marshal methods] Consolidate .ll generation into the inner build Move all marshal_methods.{abi}.ll LLVM IR generation into the inner build's RewriteMarshalMethods task, eliminating the duplicate code path in the outer build's GenerateNativeMarshalMethodSources. When marshal methods are enabled, RewriteMarshalMethods classifies, rewrites assemblies, and generates a full .ll as before. When disabled, it now generates an empty/minimal .ll with just the structural scaffolding the native runtime links against (using an empty NativeCodeGenStateObject). Strip GenerateNativeMarshalMethodSources down to P/Invoke preservation only: remove EnableMarshalMethods, EnableManagedMarshalMethodsLookup, ResolvedAssemblies, SatelliteAssemblies, AndroidRuntime properties and all .ll generation code. Remove dead code from the generator hierarchy: - MarshalMethodsNativeAssemblyGenerator: remove generateEmptyCode flag, GenerateEmptyCode property, targetArch field, and disabled constructor - MarshalMethodsNativeAssemblyGeneratorMonoVM: remove disabled ctor - MarshalMethodsNativeAssemblyGeneratorCoreCLR: remove disabled ctor and unused Xamarin.Android.Tools using The _RewriteMarshalMethodsInner target now runs unconditionally (no conditions on PublishTrimmed, _AndroidUseMarshalMethods, or AndroidIncludeDebugSymbols) and passes EnableMarshalMethods based on the _AndroidUseMarshalMethods property. --- ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 34 ++- .../GenerateNativeMarshalMethodSources.cs | 254 +++--------------- .../Tasks/RewriteMarshalMethods.cs | 81 +++--- .../MarshalMethodsNativeAssemblyGenerator.cs | 23 +- ...alMethodsNativeAssemblyGeneratorCoreCLR.cs | 8 - ...halMethodsNativeAssemblyGeneratorMonoVM.cs | 11 +- .../Xamarin.Android.Common.targets | 5 - 7 files changed, 109 insertions(+), 307 deletions(-) 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 72d48d3ddf2..6c5f2ee6f29 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 @@ -244,25 +244,37 @@ + AfterTargets="_PostTrimmingPipeline"> <_MarshalMethodsAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> -/// MSBuild task that generates native LLVM assembly source files containing marshal methods and -/// optional P/Invoke preservation code. This task creates the final native code that bridges -/// between .NET and Java/JNI, using the marshal method classifications and rewritten assemblies -/// from previous pipeline stages. +/// MSBuild task that generates P/Invoke preservation LLVM IR source files when native +/// runtime linking is enabled. Creates pinvoke_preserve.{abi}.ll files for each +/// supported ABI. /// /// -/// This task is responsible for the final code generation phase of the marshal methods pipeline: -/// -/// 1. **Marshal Methods Generation**: Creates LLVM IR code for native marshal methods that can -/// be called directly from Java/JNI without dynamic registration overhead. -/// -/// 2. **P/Invoke Preservation** (when EnableNativeRuntimeLinking=true): Generates additional -/// code to preserve P/Invoke methods that would otherwise be removed by native linking. -/// -/// 3. **Runtime-Specific Code**: Adapts the generated code for the target runtime (MonoVM or CoreCLR), -/// handling differences in runtime linking and method resolution. -/// -/// 4. **Architecture Support**: Generates separate code files for each supported Android ABI -/// (arm64-v8a, armeabi-v7a, x86_64, x86). -/// -/// The task generates LLVM IR (.ll) files that are later compiled to native assembly by the -/// Android NDK toolchain. Even when marshal methods are disabled, empty files are generated -/// to maintain build consistency. +/// Marshal method .ll generation is handled entirely by the inner build's +/// task. This task only handles P/Invoke preservation: +/// when is true, it generates additional LLVM IR +/// code that prevents the native linker from removing required P/Invoke entry points. /// public class GenerateNativeMarshalMethodSources : AndroidTask { @@ -41,21 +26,9 @@ public class GenerateNativeMarshalMethodSources : AndroidTask /// public override string TaskPrefix => "GNM"; - /// - /// Gets or sets whether to generate managed marshal methods lookup tables. - /// When enabled, creates runtime data structures for efficient marshal method resolution. - /// - public bool EnableManagedMarshalMethodsLookup { get; set; } - - /// - /// Gets or sets whether marshal methods generation is enabled. - /// When false, generates empty placeholder files to maintain build consistency. - /// - public bool EnableMarshalMethods { get; set; } - /// /// Gets or sets whether native runtime linking is enabled. - /// When true, generates additional P/Invoke preservation code to prevent + /// When true, generates P/Invoke preservation code to prevent /// native linker from removing required methods. /// public bool EnableNativeRuntimeLinking { get; set; } @@ -67,8 +40,8 @@ public class GenerateNativeMarshalMethodSources : AndroidTask public ITaskItem[] MonoComponents { get; set; } = []; /// - /// Gets or sets the output directory for environment files. - /// Generated LLVM IR files are written to this directory. + /// Gets or sets the output directory for generated files. + /// P/Invoke preservation LLVM IR files are written to this directory. /// [Required] public string EnvironmentOutputDirectory { get; set; } = ""; @@ -80,26 +53,6 @@ public class GenerateNativeMarshalMethodSources : AndroidTask [Required] public string IntermediateOutputDirectory { get; set; } = ""; - /// - /// Gets or sets the resolved assemblies to process for marshal method generation. - /// These assemblies have been processed by previous pipeline stages. - /// - [Required] - public ITaskItem [] ResolvedAssemblies { get; set; } = []; - - /// - /// Gets or sets the target Android runtime (MonoVM or CoreCLR). - /// Determines which runtime-specific code generator to use. - /// - [Required] - public string AndroidRuntime { get; set; } = ""; - - /// - /// Gets or sets the satellite assemblies containing localized resources. - /// These are included in assembly counting and naming for native code generation. - /// - public ITaskItem [] SatelliteAssemblies { get; set; } = []; - /// /// Gets or sets the list of supported Android ABIs to generate code for. /// Common values include arm64-v8a, armeabi-v7a, x86_64, and x86. @@ -107,42 +60,30 @@ public class GenerateNativeMarshalMethodSources : AndroidTask [Required] public string [] SupportedAbis { get; set; } = []; - // Parsed Android runtime type - AndroidRuntime androidRuntime; - /// - /// Executes the native marshal method source generation task. - /// Coordinates the generation of LLVM IR files for all supported Android ABIs. + /// Executes the P/Invoke preservation source generation task. /// /// /// true if the task completed successfully; false if errors occurred during processing. /// /// /// The execution flow is: - /// - /// 1. Parse the Android runtime type (MonoVM or CoreCLR) - /// 2. Retrieve native code generation state from previous pipeline stages (if marshal methods enabled) - /// 3. Generate LLVM IR files for each supported ABI - /// 4. Handle both marshal methods and P/Invoke preservation code as needed - /// - /// The native code generation state is removed from the cache after retrieval to ensure - /// it's not accidentally reused by subsequent build tasks. + /// + /// 1. Unregister (clean up) the native code generation state from the build engine cache + /// 2. If native runtime linking is enabled, generate P/Invoke preservation LLVM IR for each ABI + /// + /// The native code generation state is always unregistered to prevent accidental reuse, + /// even if P/Invoke preservation is not needed. /// public override bool RunTask () { - NativeCodeGenStateCollection? nativeCodeGenStates = null; - androidRuntime = MonoAndroidHelper.ParseAndroidRuntime (AndroidRuntime); - - // Always unregister the NativeCodeGenStateCollection to clean up, regardless - // of whether we need the data. When marshal methods are enabled, the inner - // build has already generated the .ll files; when disabled, we generate - // empty/minimal .ll files. PInvoke preservation needs the state regardless. - nativeCodeGenStates = BuildEngine4.UnregisterTaskObjectAssemblyLocal ( + // Always unregister the NativeCodeGenStateCollection to clean up. + // P/Invoke preservation needs the state when native runtime linking is enabled. + var nativeCodeGenStates = BuildEngine4.UnregisterTaskObjectAssemblyLocal ( MonoAndroidHelper.GetProjectBuildSpecificTaskObjectKey (GenerateJavaStubs.NativeCodeGenStateObjectRegisterTaskKey, WorkingDirectory, IntermediateOutputDirectory), RegisteredTaskObjectLifetime.Build ); - // Generate native code for each supported ABI foreach (var abi in SupportedAbis) Generate (nativeCodeGenStates, abi); @@ -150,168 +91,49 @@ public override bool RunTask () } /// - /// Generates native LLVM IR source files for a specific Android ABI. - /// Creates both marshal methods and optional P/Invoke preservation code. + /// Generates P/Invoke preservation LLVM IR source files for a specific Android ABI. /// /// /// Collection of native code generation states from previous pipeline stages. - /// May be null if marshal methods are disabled. + /// Required when native runtime linking is enabled. /// /// The target Android ABI to generate code for (e.g., "arm64-v8a"). /// - /// This method handles the complete code generation workflow: - /// - /// 1. **Setup**: Determines target architecture, file paths, and assembly information - /// 2. **Generator Creation**: Creates runtime-specific code generators (MonoVM or CoreCLR) - /// 3. **P/Invoke Preservation** (optional): Generates code to preserve P/Invoke methods - /// 4. **Marshal Methods**: Generates the main marshal methods LLVM IR code - /// 5. **File Output**: Writes generated code to disk with proper error handling - /// - /// The generated files are: - /// - `marshal_methods.{abi}.ll`: Main marshal methods LLVM IR - /// - `pinvoke_preserve.{abi}.ll`: P/Invoke preservation code (when native linking enabled) - /// - /// Both generators construct an LLVM IR module and then generate the actual code, - /// with proper stream management and error recovery in case of partial writes. + /// When is false, this method returns immediately. + /// Otherwise it generates pinvoke_preserve.{abi}.ll containing references to + /// P/Invoke entry points that must survive native linking. /// void Generate (NativeCodeGenStateCollection? nativeCodeGenStates, string abi) { - // Setup target information and file paths - var targetAbi = abi.ToLowerInvariant (); - var targetArch = MonoAndroidHelper.AbiToTargetArch (abi); - var marshalMethodsBaseAsmFilePath = Path.Combine (EnvironmentOutputDirectory, $"marshal_methods.{targetAbi}"); - var pinvokePreserveBaseAsmFilePath = EnableNativeRuntimeLinking ? Path.Combine (EnvironmentOutputDirectory, $"pinvoke_preserve.{targetAbi}") : null; - var marshalMethodsLlFilePath = $"{marshalMethodsBaseAsmFilePath}.ll"; - var pinvokePreserveLlFilePath = pinvokePreserveBaseAsmFilePath != null ? $"{pinvokePreserveBaseAsmFilePath}.ll" : null; - - // Generate P/Invoke preservation code if native linking is enabled - bool fileFullyWritten; - if (EnableNativeRuntimeLinking) { - var pinvokePreserveGen = new PreservePinvokesNativeAssemblyGenerator (Log, EnsureCodeGenState (nativeCodeGenStates, targetArch), MonoComponents); - LLVMIR.LlvmIrModule pinvokePreserveModule = pinvokePreserveGen.Construct (); - using var pinvokePreserveWriter = MemoryStreamPool.Shared.CreateStreamWriter (); - fileFullyWritten = false; - try { - pinvokePreserveGen.Generate (pinvokePreserveModule, targetArch, pinvokePreserveWriter, pinvokePreserveLlFilePath!); - pinvokePreserveWriter.Flush (); - Files.CopyIfStreamChanged (pinvokePreserveWriter.BaseStream, pinvokePreserveLlFilePath!); - fileFullyWritten = true; - } finally { - // Log partial contents for debugging if generation failed - if (!fileFullyWritten) { - MonoAndroidHelper.LogTextStreamContents (Log, $"Partial contents of file '{pinvokePreserveLlFilePath}'", pinvokePreserveWriter.BaseStream); - } - } - } - - // When marshal methods are enabled, the inner build has already generated the - // marshal_methods..ll file — skip generation here. - if (EnableMarshalMethods) { - Log.LogDebugMessage ($"Marshal methods .ll file for '{targetAbi}' was generated by the inner build, skipping outer build generation."); + if (!EnableNativeRuntimeLinking) { return; } - // Marshal methods are disabled — generate empty/minimal .ll files - var (assemblyCount, uniqueAssemblyNames) = GetAssemblyCountAndUniqueNames (); - - // Create the appropriate runtime-specific generator (disabled path — empty/minimal code) - MarshalMethodsNativeAssemblyGenerator marshalMethodsAsmGen = androidRuntime switch { - Tasks.AndroidRuntime.MonoVM => new MarshalMethodsNativeAssemblyGeneratorMonoVM ( - Log, - targetArch, - assemblyCount, - uniqueAssemblyNames - ), - Tasks.AndroidRuntime.CoreCLR => new MarshalMethodsNativeAssemblyGeneratorCoreCLR ( - Log, - targetArch, - uniqueAssemblyNames - ), - _ => throw new NotSupportedException ($"Internal error: unsupported runtime type '{androidRuntime}'") - }; - - // Generate marshal methods code - var marshalMethodsModule = marshalMethodsAsmGen.Construct (); - using var marshalMethodsWriter = MemoryStreamPool.Shared.CreateStreamWriter (); + // Generate P/Invoke preservation code + var targetAbi = abi.ToLowerInvariant (); + var targetArch = MonoAndroidHelper.AbiToTargetArch (abi); + var pinvokePreserveLlFilePath = Path.Combine (EnvironmentOutputDirectory, $"pinvoke_preserve.{targetAbi}.ll"); - fileFullyWritten = false; + var pinvokePreserveGen = new PreservePinvokesNativeAssemblyGenerator (Log, EnsureCodeGenState (nativeCodeGenStates, targetArch), MonoComponents); + LLVMIR.LlvmIrModule pinvokePreserveModule = pinvokePreserveGen.Construct (); + using var pinvokePreserveWriter = MemoryStreamPool.Shared.CreateStreamWriter (); + bool fileFullyWritten = false; try { - marshalMethodsAsmGen.Generate (marshalMethodsModule, targetArch, marshalMethodsWriter, marshalMethodsLlFilePath); - marshalMethodsWriter.Flush (); - Files.CopyIfStreamChanged (marshalMethodsWriter.BaseStream, marshalMethodsLlFilePath); + pinvokePreserveGen.Generate (pinvokePreserveModule, targetArch, pinvokePreserveWriter, pinvokePreserveLlFilePath); + pinvokePreserveWriter.Flush (); + Files.CopyIfStreamChanged (pinvokePreserveWriter.BaseStream, pinvokePreserveLlFilePath); fileFullyWritten = true; } finally { - // Log partial contents for debugging if generation failed if (!fileFullyWritten) { - MonoAndroidHelper.LogTextStreamContents (Log, $"Partial contents of file '{marshalMethodsLlFilePath}'", marshalMethodsWriter.BaseStream); + MonoAndroidHelper.LogTextStreamContents (Log, $"Partial contents of file '{pinvokePreserveLlFilePath}'", pinvokePreserveWriter.BaseStream); } } } - /// - /// Counts the total number of assemblies and collects unique assembly names - /// from both resolved assemblies and satellite assemblies. - /// - /// - /// A tuple containing: - /// - assemblyCount: The total number of unique assemblies across all architectures - /// - uniqueAssemblyNames: A set of unique assembly names including culture information - /// - /// - /// This method processes both main assemblies and satellite assemblies (for localization). - /// For satellite assemblies, the culture name is prepended to create unique identifiers - /// (e.g., "en-US/MyApp.resources.dll"). This information is used by the native code - /// generators to create appropriate lookup structures and assembly metadata. - /// - (int assemblyCount, HashSet uniqueAssemblyNames) GetAssemblyCountAndUniqueNames () - { - var assemblyCount = 0; - var archAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); - var uniqueAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); - - // Process both main assemblies and satellite assemblies - foreach (var assembly in SatelliteAssemblies.Concat (ResolvedAssemblies)) { - var culture = MonoAndroidHelper.GetAssemblyCulture (assembly); - var fileName = Path.GetFileName (assembly.ItemSpec); - string assemblyName; - - // Include culture information for satellite assemblies - if (culture.IsNullOrEmpty ()) { - assemblyName = fileName; - } else { - assemblyName = $"{culture}/{fileName}"; - } - - // Track all unique assembly names across architectures - uniqueAssemblyNames.Add (assemblyName); - - // Count unique assemblies per architecture to avoid duplicates - if (!archAssemblyNames.Contains (assemblyName)) { - assemblyCount++; - archAssemblyNames.Add (assemblyName); - } - } - - return (assemblyCount, uniqueAssemblyNames); - } - /// /// Retrieves the native code generation state for a specific target architecture. /// Validates that the required state exists and throws an exception if missing. /// - /// - /// The collection of native code generation states from previous pipeline stages. - /// - /// The target architecture to retrieve state for. - /// The native code generation state for the specified architecture. - /// - /// Thrown when the state collection is null or doesn't contain state for the target architecture. - /// - /// - /// This method ensures that the required native code generation state is available - /// before attempting to generate marshal methods code. The state contains marshal method - /// classifications, assembly information, and other data needed for code generation. - /// NativeCodeGenStateObject EnsureCodeGenState (NativeCodeGenStateCollection? nativeCodeGenStates, AndroidTargetArch targetArch) { if (nativeCodeGenStates is null || !nativeCodeGenStates.States.TryGetValue (targetArch, out NativeCodeGenStateObject? state)) { diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs index e3351d31269..7204621ccf5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs @@ -10,31 +10,29 @@ namespace Xamarin.Android.Tasks; /// -/// MSBuild task that classifies, rewrites, and generates LLVM IR for marshal methods in -/// the inner (per-RID) build. Runs after ILLink and _PostTrimmingPipeline on the trimmed -/// assemblies, and before ReadyToRun/crossgen2 so that R2R images are built from the -/// rewritten assemblies. +/// MSBuild task that runs in the inner (per-RID) build to generate the +/// marshal_methods.{abi}.ll LLVM IR file. /// -/// The task performs the following steps: +/// When marshal methods are enabled (Release + trimmed), the task also classifies +/// marshal methods, rewrites assemblies in-place (adds [UnmanagedCallersOnly] +/// wrappers, removes connectors), and generates a full .ll with native marshal +/// method functions. /// -/// 1. Opens the trimmed assemblies with Cecil and classifies marshal methods via -/// -/// 2. Rewrites assemblies in-place: adds [UnmanagedCallersOnly] wrappers, removes -/// connector methods and callback delegate backing fields -/// 3. Generates the marshal_methods.{abi}.ll LLVM IR file into -/// (the outer build's intermediate dir) +/// When marshal methods are disabled (Debug, or Release without marshal methods), +/// the task generates an empty/minimal .ll containing only the structural +/// scaffolding the native runtime always links against. /// -/// Because this runs in the inner build, the outer build sees already-rewritten assemblies -/// in @(ResolvedFileToPublish). Downstream consumers -/// (_AfterILLinkAdditionalSteps, GenerateTypeMappings) therefore work on -/// post-rewrite tokens, eliminating the token staleness problem. +/// Runs AfterTargets="_PostTrimmingPipeline" (which is AfterTargets="ILLink") so +/// that trimmed assemblies are available for rewriting when trimming is active. +/// MSBuild fires AfterTargets hooks even when the referenced target is +/// condition-skipped, so this target also runs in untrimmed builds. /// public class RewriteMarshalMethods : AndroidTask { public override string TaskPrefix => "RMM"; /// - /// The trimmed assemblies to process (from @(ResolvedFileToPublish) filtered to .dll). + /// The assemblies to process (from @(ResolvedFileToPublish) filtered to .dll). /// [Required] public ITaskItem [] Assemblies { get; set; } = []; @@ -52,21 +50,21 @@ public class RewriteMarshalMethods : AndroidTask public bool EnableManagedMarshalMethodsLookup { get; set; } /// - /// Whether marshal methods are enabled. Should always be true when this task - /// is invoked, but is kept as a property for clarity and consistency with the target - /// condition. + /// Whether marshal methods are enabled. When false, the task skips classification + /// and rewriting and generates an empty/minimal .ll file. /// public bool EnableMarshalMethods { get; set; } /// /// Environment files to parse for configuration (e.g. XA_BROKEN_EXCEPTION_TRANSITIONS). + /// Only used when marshal methods are enabled. /// public ITaskItem [] Environments { get; set; } = []; /// /// Directory where the marshal_methods.{abi}.ll file is written. - /// Typically $(_OuterIntermediateOutputPath)android so the outer build can - /// find it via @(_MarshalMethodsAssemblySource). + /// Typically $(_OuterIntermediateOutputPath)android so the outer build's + /// _CompileNativeAssemblySources can compile it. /// [Required] public string MarshalMethodsOutputDirectory { get; set; } = ""; @@ -80,28 +78,30 @@ public class RewriteMarshalMethods : AndroidTask public override bool RunTask () { - if (!EnableMarshalMethods) { - Log.LogDebugMessage ("Marshal methods are not enabled, skipping."); - return true; - } - var androidRuntime = MonoAndroidHelper.ParseAndroidRuntime (AndroidRuntime); - // Parse environment files for configuration (e.g. broken exception transitions) - var environmentParser = new EnvironmentFilesParser (); - bool brokenExceptionTransitionsEnabled = environmentParser.AreBrokenExceptionTransitionsEnabled (Environments); - string abi = MonoAndroidHelper.RidToAbi (RuntimeIdentifier); var targetArch = MonoAndroidHelper.AbiToTargetArch (abi); - ProcessArchitecture (targetArch, abi, androidRuntime, brokenExceptionTransitionsEnabled); + + if (EnableMarshalMethods) { + ProcessMarshalMethods (targetArch, abi, androidRuntime); + } else { + GenerateEmptyLlvmIr (targetArch, abi, androidRuntime); + } return !Log.HasLoggedErrors; } - void ProcessArchitecture (AndroidTargetArch targetArch, string abi, AndroidRuntime androidRuntime, bool brokenExceptionTransitionsEnabled) + /// + /// Marshal methods enabled path: classify, rewrite assemblies, generate full .ll. + /// + void ProcessMarshalMethods (AndroidTargetArch targetArch, string abi, AndroidRuntime androidRuntime) { + // Parse environment files for configuration (e.g. broken exception transitions) + var environmentParser = new EnvironmentFilesParser (); + bool brokenExceptionTransitionsEnabled = environmentParser.AreBrokenExceptionTransitionsEnabled (Environments); + // Step 1: Open assemblies with Cecil and classify marshal methods - // Build the dictionary keyed by assembly name that MakeResolver and FromAssemblies expect var assemblyDict = new Dictionary (StringComparer.OrdinalIgnoreCase); foreach (var item in Assemblies) { var name = Path.GetFileNameWithoutExtension (item.ItemSpec); @@ -125,8 +125,6 @@ void ProcessArchitecture (AndroidTargetArch targetArch, string abi, AndroidRunti RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled); classifier.AddSpecialCaseMethods (); } else { - // When managed lookup is enabled, add special cases first so they - // appear in the lookup tables classifier.AddSpecialCaseMethods (); var lookupInfo = new ManagedMarshalMethodsLookupInfo (Log); RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled, lookupInfo); @@ -134,7 +132,7 @@ void ProcessArchitecture (AndroidTargetArch targetArch, string abi, AndroidRunti ReportStatistics (targetArch, classifier); - // Step 3: Build NativeCodeGenStateObject from Cecil state and generate .ll + // Step 3: Build NativeCodeGenStateObject and generate .ll var codeGenState = MarshalMethodCecilAdapter.CreateNativeCodeGenStateObjectFromClassifier (targetArch, classifier); GenerateLlvmIr (targetArch, abi, androidRuntime, codeGenState); @@ -142,6 +140,17 @@ void ProcessArchitecture (AndroidTargetArch targetArch, string abi, AndroidRunti resolver.Dispose (); } + /// + /// Marshal methods disabled path: generate empty/minimal .ll with structural scaffolding only. + /// + void GenerateEmptyLlvmIr (AndroidTargetArch targetArch, string abi, AndroidRuntime androidRuntime) + { + var emptyCodeGenState = new NativeCodeGenStateObject { + TargetArch = targetArch, + }; + GenerateLlvmIr (targetArch, abi, androidRuntime, emptyCodeGenState); + } + void RewriteAssemblies (AndroidTargetArch targetArch, MarshalMethodsCollection classifier, XAAssemblyResolver resolver, bool brokenExceptionTransitionsEnabled, ManagedMarshalMethodsLookupInfo? lookupInfo = null) { var rewriter = new MarshalMethodsAssemblyRewriter (Log, targetArch, classifier, resolver, lookupInfo); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs index 950f89530bc..9b326926ff0 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGenerator.cs @@ -209,29 +209,11 @@ public MarshalMethodAssemblyIndexValuePlaceholder (MarshalMethodInfo mmi, Assemb #pragma warning disable CS0414 // Field is assigned but its value is never used - might be used for debugging or future functionality readonly LlvmIrCallMarker defaultCallMarker; #pragma warning restore CS0414 - readonly bool generateEmptyCode; readonly bool managedMarshalMethodsLookupEnabled; - readonly AndroidTargetArch targetArch; readonly NativeCodeGenStateObject? codeGenState; - protected bool GenerateEmptyCode => generateEmptyCode; protected List Methods => methods; - /// - /// Constructor to be used ONLY when marshal methods are DISABLED - /// - protected MarshalMethodsNativeAssemblyGenerator (TaskLoggingHelper log, AndroidTargetArch targetArch, ICollection uniqueAssemblyNames) - : base (log) - { - this.targetArch = targetArch; - this.uniqueAssemblyNames = uniqueAssemblyNames ?? throw new ArgumentNullException (nameof (uniqueAssemblyNames)); - generateEmptyCode = true; - defaultCallMarker = LlvmIrCallMarker.Tail; - } - - /// - /// Constructor to be used ONLY when marshal methods are ENABLED - /// protected MarshalMethodsNativeAssemblyGenerator (TaskLoggingHelper log, ICollection uniqueAssemblyNames, NativeCodeGenStateObject codeGenState, bool managedMarshalMethodsLookupEnabled) : base (log) { @@ -239,13 +221,12 @@ protected MarshalMethodsNativeAssemblyGenerator (TaskLoggingHelper log, ICollect this.codeGenState = codeGenState ?? throw new ArgumentNullException (nameof (codeGenState)); this.managedMarshalMethodsLookupEnabled = managedMarshalMethodsLookupEnabled; - generateEmptyCode = false; defaultCallMarker = LlvmIrCallMarker.Tail; } void Init () { - if (generateEmptyCode || codeGenState.MarshalMethods.Count == 0) { + if (codeGenState.MarshalMethods.Count == 0) { return; } @@ -607,7 +588,7 @@ protected virtual void AddClassCache (LlvmIrModule module) void AddMarshalMethods (LlvmIrModule module, AssemblyCacheState acs, LlvmIrVariable getFunctionPtrVariable, LlvmIrFunction getFunctionPtrFunction) { - if (generateEmptyCode || methods == null || methods.Count == 0) { + if (methods == null || methods.Count == 0) { return; } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGeneratorCoreCLR.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGeneratorCoreCLR.cs index e57a943afe9..16534e82abf 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGeneratorCoreCLR.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGeneratorCoreCLR.cs @@ -1,19 +1,11 @@ #nullable enable using System.Collections.Generic; using Microsoft.Build.Utilities; -using Xamarin.Android.Tools; namespace Xamarin.Android.Tasks; class MarshalMethodsNativeAssemblyGeneratorCoreCLR : MarshalMethodsNativeAssemblyGenerator { - /// - /// Constructor to be used ONLY when marshal methods are DISABLED - /// - public MarshalMethodsNativeAssemblyGeneratorCoreCLR (TaskLoggingHelper log, AndroidTargetArch targetArch, ICollection uniqueAssemblyNames) - : base (log, targetArch, uniqueAssemblyNames) - {} - public MarshalMethodsNativeAssemblyGeneratorCoreCLR (TaskLoggingHelper log, ICollection uniqueAssemblyNames, NativeCodeGenStateObject codeGenState, bool managedMarshalMethodsLookupEnabled) : base (log, uniqueAssemblyNames, codeGenState, managedMarshalMethodsLookupEnabled) {} diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGeneratorMonoVM.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGeneratorMonoVM.cs index 8409dd6bc61..ad10255a376 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGeneratorMonoVM.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsNativeAssemblyGeneratorMonoVM.cs @@ -42,15 +42,6 @@ sealed class MarshalMethodName readonly int numberOfAssembliesInApk; StructureInfo? marshalMethodNameStructureInfo; - /// - /// Constructor to be used ONLY when marshal methods are DISABLED - /// - public MarshalMethodsNativeAssemblyGeneratorMonoVM (TaskLoggingHelper log, AndroidTargetArch targetArch, int numberOfAssembliesInApk, ICollection uniqueAssemblyNames) - : base (log, targetArch, uniqueAssemblyNames) - { - this.numberOfAssembliesInApk = numberOfAssembliesInApk; - } - public MarshalMethodsNativeAssemblyGeneratorMonoVM (TaskLoggingHelper log, int numberOfAssembliesInApk, ICollection uniqueAssemblyNames, NativeCodeGenStateObject codeGenState, bool managedMarshalMethodsLookupEnabled) : base (log, uniqueAssemblyNames, codeGenState, managedMarshalMethodsLookupEnabled) { @@ -61,7 +52,7 @@ protected override void AddMarshalMethodNames (LlvmIrModule module, AssemblyCach { var uniqueMethods = new Dictionary (); - if (!GenerateEmptyCode && Methods != null) { + if (Methods != null) { foreach (MarshalMethodInfo mmi in Methods) { string asmName = Path.GetFileName (mmi.Method.NativeCallback.DeclaringType.Module.Assembly.MainModuleFileName); diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index c2527d1954e..30cff04e4b0 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1758,15 +1758,10 @@ because xbuild doesn't support framework reference assemblies. From e92f56e559bdb6c204a036877f302a6b0afc9e78 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 9 Apr 2026 06:10:46 -0700 Subject: [PATCH 3/3] [marshal methods] Write rewritten assemblies to separate output directory Instead of rewriting assemblies in-place (which fails when multiple RIDs share the same input path, e.g. NuGet runtime pack with PublishTrimmed=false), write rewritten assemblies to a per-RID output directory and update @(ResolvedFileToPublish) items so downstream processing uses the copies. This removes the PublishTrimmed gate entirely - EnableMarshalMethods is now passed directly as $(_AndroidUseMarshalMethods). --- ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 23 ++- .../Tasks/RewriteMarshalMethods.cs | 132 +++++++++++++++--- .../MarshalMethodsAssemblyRewriter.cs | 88 +++--------- 3 files changed, 153 insertions(+), 90 deletions(-) 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 6c5f2ee6f29..169d3ffaecd 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 @@ -247,10 +247,13 @@ Inner-build marshal method handling. Always runs (for every RID) to generate the marshal_methods..ll LLVM IR file. - When marshal methods are enabled (Release + PublishTrimmed), the task also - classifies marshal methods, rewrites assemblies (adds [UnmanagedCallersOnly] - wrappers, removes connectors), and generates a full .ll with marshal method - native functions. + When marshal methods are enabled (Release + MonoVM), the task also classifies + marshal methods, rewrites assemblies (adds [UnmanagedCallersOnly] wrappers, + removes connectors), and generates a full .ll with marshal method native + functions. Rewritten assemblies are always written to a separate per-RID + output directory (never in-place), so parallel inner builds don't conflict + even when the input assemblies point to a shared location (e.g. the NuGet + runtime pack when PublishTrimmed is false). When marshal methods are disabled (Debug, or Release without marshal methods), the task generates an empty/minimal .ll containing only the structural @@ -277,8 +280,18 @@ EnableMarshalMethods="$(_AndroidUseMarshalMethods)" Environments="@(_EnvironmentFiles)" MarshalMethodsOutputDirectory="$(_OuterIntermediateOutputPath)android" + RewrittenAssembliesOutputDirectory="$(IntermediateOutputPath)rewritten-marshal-methods" AndroidRuntime="$(_AndroidRuntime)" - RuntimeIdentifier="$(RuntimeIdentifier)" /> + RuntimeIdentifier="$(RuntimeIdentifier)"> + + + + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs index 7204621ccf5..bf15873404c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs @@ -5,6 +5,7 @@ using System.Linq; using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; using Xamarin.Android.Tools; namespace Xamarin.Android.Tasks; @@ -13,19 +14,25 @@ namespace Xamarin.Android.Tasks; /// MSBuild task that runs in the inner (per-RID) build to generate the /// marshal_methods.{abi}.ll LLVM IR file. /// -/// When marshal methods are enabled (Release + trimmed), the task also classifies -/// marshal methods, rewrites assemblies in-place (adds [UnmanagedCallersOnly] -/// wrappers, removes connectors), and generates a full .ll with native marshal -/// method functions. +/// When marshal methods are enabled (Release + MonoVM), the task classifies +/// marshal methods, rewrites assemblies (adds [UnmanagedCallersOnly] wrappers, +/// removes connectors), and generates a full .ll with native marshal method +/// functions. Rewritten assemblies are written to a separate per-RID output +/// directory (), never in-place, +/// so that parallel inner builds don't conflict even when the input assemblies +/// point to a shared location (e.g. the NuGet runtime pack when PublishTrimmed +/// is false). The output items () allow the +/// target to update @(ResolvedFileToPublish) so downstream processing +/// uses the rewritten copies. /// -/// When marshal methods are disabled (Debug, or Release without marshal methods), -/// the task generates an empty/minimal .ll containing only the structural -/// scaffolding the native runtime always links against. +/// When marshal methods are disabled (Debug, or Release without marshal +/// methods), the task generates an empty/minimal .ll containing only the +/// structural scaffolding the native runtime always links against. /// -/// Runs AfterTargets="_PostTrimmingPipeline" (which is AfterTargets="ILLink") so -/// that trimmed assemblies are available for rewriting when trimming is active. -/// MSBuild fires AfterTargets hooks even when the referenced target is -/// condition-skipped, so this target also runs in untrimmed builds. +/// Runs AfterTargets="_PostTrimmingPipeline" (which is AfterTargets="ILLink") +/// so that trimmed assemblies are available for rewriting when trimming is +/// active. MSBuild fires AfterTargets hooks even when the referenced target +/// is condition-skipped, so this target also runs in untrimmed builds. /// public class RewriteMarshalMethods : AndroidTask { @@ -69,6 +76,16 @@ public class RewriteMarshalMethods : AndroidTask [Required] public string MarshalMethodsOutputDirectory { get; set; } = ""; + /// + /// Per-RID directory where rewritten assemblies are written. Rewritten + /// assemblies are always written to this directory (never in-place) so that + /// parallel inner builds for different RIDs don't conflict, even when the + /// input assemblies reside in a shared location such as the NuGet runtime pack. + /// Only used when is true. + /// + [Required] + public string RewrittenAssembliesOutputDirectory { get; set; } = ""; + /// /// The RuntimeIdentifier for this inner build (e.g. android-arm64). /// Converted to an ABI and target architecture internally. @@ -76,6 +93,17 @@ public class RewriteMarshalMethods : AndroidTask [Required] public string RuntimeIdentifier { get; set; } = ""; + /// + /// Output: assemblies (and PDBs) that were rewritten to + /// . Each item carries all + /// metadata from the original input assembly plus OriginalItemSpec + /// pointing to the original path. The calling target uses these to replace + /// the original items in @(ResolvedFileToPublish) so downstream + /// processing picks up the rewritten copies. + /// + [Output] + public ITaskItem []? RewrittenAssemblies { get; set; } + public override bool RunTask () { var androidRuntime = MonoAndroidHelper.ParseAndroidRuntime (AndroidRuntime); @@ -83,6 +111,11 @@ public override bool RunTask () string abi = MonoAndroidHelper.RidToAbi (RuntimeIdentifier); var targetArch = MonoAndroidHelper.AbiToTargetArch (abi); + // The inner build's @(ResolvedFileToPublish) items don't have %(Abi) metadata yet — + // that's normally stamped later by ProcessAssemblies in the outer build. Downstream + // code (XAJavaTypeScanner) reads %(Abi) from each ITaskItem, so we need to set it here. + EnsureAbiMetadata (abi); + if (EnableMarshalMethods) { ProcessMarshalMethods (targetArch, abi, androidRuntime); } else { @@ -93,7 +126,7 @@ public override bool RunTask () } /// - /// Marshal methods enabled path: classify, rewrite assemblies, generate full .ll. + /// Marshal methods enabled path: classify, rewrite assemblies to output directory, generate full .ll. /// void ProcessMarshalMethods (AndroidTargetArch targetArch, string abi, AndroidRuntime androidRuntime) { @@ -120,23 +153,27 @@ void ProcessMarshalMethods (AndroidTargetArch targetArch, string abi, AndroidRun return; } - // Step 2: Rewrite assemblies + // Step 2: Rewrite assemblies to the per-RID output directory + HashSet rewrittenOriginalPaths; if (!EnableManagedMarshalMethodsLookup) { - RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled); + rewrittenOriginalPaths = RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled); classifier.AddSpecialCaseMethods (); } else { classifier.AddSpecialCaseMethods (); var lookupInfo = new ManagedMarshalMethodsLookupInfo (Log); - RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled, lookupInfo); + rewrittenOriginalPaths = RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled, lookupInfo); } ReportStatistics (targetArch, classifier); - // Step 3: Build NativeCodeGenStateObject and generate .ll + // Step 3: Build output items for rewritten assemblies (DLLs and PDBs) + BuildRewrittenAssembliesOutput (rewrittenOriginalPaths); + + // Step 4: Build NativeCodeGenStateObject and generate .ll var codeGenState = MarshalMethodCecilAdapter.CreateNativeCodeGenStateObjectFromClassifier (targetArch, classifier); GenerateLlvmIr (targetArch, abi, androidRuntime, codeGenState); - // Step 4: Dispose Cecil resolvers + // Step 5: Dispose Cecil resolvers resolver.Dispose (); } @@ -151,10 +188,50 @@ void GenerateEmptyLlvmIr (AndroidTargetArch targetArch, string abi, AndroidRunti GenerateLlvmIr (targetArch, abi, androidRuntime, emptyCodeGenState); } - void RewriteAssemblies (AndroidTargetArch targetArch, MarshalMethodsCollection classifier, XAAssemblyResolver resolver, bool brokenExceptionTransitionsEnabled, ManagedMarshalMethodsLookupInfo? lookupInfo = null) + HashSet RewriteAssemblies (AndroidTargetArch targetArch, MarshalMethodsCollection classifier, XAAssemblyResolver resolver, bool brokenExceptionTransitionsEnabled, ManagedMarshalMethodsLookupInfo? lookupInfo = null) { var rewriter = new MarshalMethodsAssemblyRewriter (Log, targetArch, classifier, resolver, lookupInfo); - rewriter.Rewrite (brokenExceptionTransitionsEnabled); + return rewriter.Rewrite (brokenExceptionTransitionsEnabled, RewrittenAssembliesOutputDirectory); + } + + /// + /// Build output items for assemblies (and PDBs) that were rewritten to the output directory. + /// Each output item has the rewritten path as its ItemSpec, all metadata copied from the + /// corresponding input assembly, and OriginalItemSpec set to the original path. + /// + void BuildRewrittenAssembliesOutput (HashSet rewrittenOriginalPaths) + { + if (rewrittenOriginalPaths.Count == 0) { + return; + } + + var rewrittenItems = new List (); + + foreach (var item in Assemblies) { + if (!rewrittenOriginalPaths.Contains (item.ItemSpec)) { + continue; + } + + string rewrittenPath = Path.Combine (RewrittenAssembliesOutputDirectory, Path.GetFileName (item.ItemSpec)); + + // Output item for the rewritten DLL + var dllItem = new TaskItem (rewrittenPath); + item.CopyMetadataTo (dllItem); + dllItem.SetMetadata ("OriginalItemSpec", item.ItemSpec); + rewrittenItems.Add (dllItem); + + // Output item for the rewritten PDB, if one was produced + string rewrittenPdb = Path.ChangeExtension (rewrittenPath, ".pdb"); + if (File.Exists (rewrittenPdb)) { + string originalPdb = Path.ChangeExtension (item.ItemSpec, ".pdb"); + var pdbItem = new TaskItem (rewrittenPdb); + item.CopyMetadataTo (pdbItem); + pdbItem.SetMetadata ("OriginalItemSpec", originalPdb); + rewrittenItems.Add (pdbItem); + } + } + + RewrittenAssemblies = rewrittenItems.ToArray (); } void ReportStatistics (AndroidTargetArch targetArch, MarshalMethodsCollection classifier) @@ -214,6 +291,23 @@ void GenerateLlvmIr (AndroidTargetArch targetArch, string abi, AndroidRuntime an } } + /// + /// Stamp %(Abi) metadata on every assembly item that doesn't already have it. + /// The inner build's @(ResolvedFileToPublish) items carry %(RuntimeIdentifier) + /// but not %(Abi) — that is normally set later by ProcessAssemblies in the + /// outer build. Downstream code (XAJavaTypeScanner) reads %(Abi) from each + /// ITaskItem, so we set it here from the task's RuntimeIdentifier parameter. + /// + void EnsureAbiMetadata (string abi) + { + foreach (var item in Assemblies) { + string? existingAbi = item.GetMetadata ("Abi"); + if (existingAbi.IsNullOrEmpty ()) { + item.SetMetadata ("Abi", abi); + } + } + } + (int assemblyCount, HashSet uniqueAssemblyNames) GetAssemblyCountAndUniqueNames () { var assemblyCount = 0; diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs index 807632241ba..4f028c3ae52 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodsAssemblyRewriter.cs @@ -39,8 +39,8 @@ public MarshalMethodsAssemblyRewriter (TaskLoggingHelper log, AndroidTargetArch this.managedMarshalMethodsLookupInfo = managedMarshalMethodsLookupInfo; } - // TODO: do away with broken exception transitions, there's no point in supporting them - public void Rewrite (bool brokenExceptionTransitions) + // TODO: do away with broken exception transitions, there's no point in supporting them + public HashSet Rewrite (bool brokenExceptionTransitions, string outputDirectory) { AssemblyDefinition? monoAndroidRuntime = resolver.Resolve ("Mono.Android.Runtime"); if (monoAndroidRuntime == null) { @@ -141,78 +141,34 @@ public void Rewrite (bool brokenExceptionTransitions) managedMarshalMethodLookupGenerator.Generate (classifier.MarshalMethods.Values); } - foreach (AssemblyDefinition asm in classifier.AssembliesWithMarshalMethods) { - string? path = asm.MainModule.FileName; - if (String.IsNullOrEmpty (path)) { - throw new InvalidOperationException ($"[{targetArch}] Internal error: assembly '{asm}' does not specify path to its file"); - } - - string pathPdb = Path.ChangeExtension (path, ".pdb"); - bool havePdb = File.Exists (pathPdb); - - var writerParams = new WriterParameters { - WriteSymbols = havePdb, - }; - - string directory = Path.Combine (Path.GetDirectoryName (path), "new"); - Directory.CreateDirectory (directory); - string output = Path.Combine (directory, Path.GetFileName (path)); - log.LogDebugMessage ($"[{targetArch}] Writing new version of '{path}' assembly: {output}"); + var rewrittenOriginalPaths = new HashSet (StringComparer.OrdinalIgnoreCase); + Directory.CreateDirectory (outputDirectory); - // TODO: this should be used eventually, but it requires that all the types are reloaded from the assemblies before typemaps are generated - // since Cecil doesn't update the MVID in the already loaded types - //asm.MainModule.Mvid = Guid.NewGuid (); - asm.Write (output, writerParams); - - CopyFile (output, path); - RemoveFile (output); - - if (havePdb) { - string outputPdb = Path.ChangeExtension (output, ".pdb"); - if (File.Exists (outputPdb)) { - CopyFile (outputPdb, pathPdb); - } - RemoveFile (outputPdb); - } + foreach (AssemblyDefinition asm in classifier.AssembliesWithMarshalMethods) { + string? path = asm.MainModule.FileName; + if (String.IsNullOrEmpty (path)) { + throw new InvalidOperationException ($"[{targetArch}] Internal error: assembly '{asm}' does not specify path to its file"); } - void CopyFile (string source, string target) - { - log.LogDebugMessage ($"[{targetArch}] Copying rewritten assembly: {source} -> {target}"); + string pathPdb = Path.ChangeExtension (path, ".pdb"); + bool havePdb = File.Exists (pathPdb); - string targetBackup = $"{target}.bak"; - if (File.Exists (target)) { - // Try to avoid sharing violations by first renaming the target - File.Move (target, targetBackup); - } + var writerParams = new WriterParameters { + WriteSymbols = havePdb, + }; - File.Copy (source, target, true); + string output = Path.Combine (outputDirectory, Path.GetFileName (path)); + log.LogDebugMessage ($"[{targetArch}] Writing rewritten assembly: {path} -> {output}"); - if (File.Exists (targetBackup)) { - try { - File.Delete (targetBackup); - } catch (Exception ex) { - // On Windows the deletion may fail, depending on lock state of the original `target` file before the move. - log.LogDebugMessage ($"[{targetArch}] While trying to delete '{targetBackup}', exception was thrown: {ex}"); - log.LogDebugMessage ($"[{targetArch}] Failed to delete backup file '{targetBackup}', ignoring."); - } - } - } + // TODO: this should be used eventually, but it requires that all the types are reloaded from the assemblies before typemaps are generated + // since Cecil doesn't update the MVID in the already loaded types + //asm.MainModule.Mvid = Guid.NewGuid (); + asm.Write (output, writerParams); - void RemoveFile (string? path) - { - if (String.IsNullOrEmpty (path) || !File.Exists (path)) { - return; - } + rewrittenOriginalPaths.Add (path); + } - try { - log.LogDebugMessage ($"[{targetArch}] Deleting: {path}"); - File.Delete (path); - } catch (Exception ex) { - log.LogWarning ($"[{targetArch}] Unable to delete source file '{path}'"); - log.LogDebugMessage ($"[{targetArch}] {ex.ToString ()}"); - } - } + return rewrittenOriginalPaths; static bool HasUnmanagedCallersOnlyAttribute (MethodDefinition method) {