From d8657666d547bf0bacefe58d4f1a12e0b34cc634 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 3 Apr 2026 11:31:16 -0700 Subject: [PATCH 1/6] [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/6] [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/6] [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) { From 8857691f4ed88f3cbfa3cd1420472dc6bdab2a89 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Mon, 13 Apr 2026 13:57:06 -0700 Subject: [PATCH 4/6] [marshal methods] Skip NativeAOT builds and fix PDB output items Exclude _RewriteMarshalMethodsInner from NativeAOT builds (which don't use .ll-based marshal methods) and remove PDB output items that caused NETSDK1152 duplicate relative path errors. --- ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 14 +++++++--- .../Tasks/RewriteMarshalMethods.cs | 27 +++++++++---------- 2 files changed, 22 insertions(+), 19 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 169d3ffaecd..9a1cf6cd561 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 @@ -268,9 +268,13 @@ The .ll file is written to $(_OuterIntermediateOutputPath)android/ so the outer build's _CompileNativeAssemblySources can compile it. + + Skipped for NativeAOT because it does not use .ll-based marshal methods + or native assembly sources (those targets are also NativeAOT-excluded). --> + AfterTargets="_PostTrimmingPipeline" + Condition=" '$(_AndroidRuntime)' != 'NativeAOT' "> <_MarshalMethodsAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> @@ -285,9 +289,11 @@ RuntimeIdentifier="$(RuntimeIdentifier)"> - + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs index bf15873404c..3605093d70f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs @@ -94,12 +94,14 @@ public class RewriteMarshalMethods : AndroidTask public string RuntimeIdentifier { get; set; } = ""; /// - /// Output: assemblies (and PDBs) that were rewritten to + /// Output: rewritten assembly DLLs in /// . 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. + /// processing picks up the rewritten copies. PDB files are NOT included — + /// they sit alongside the DLLs and are discovered by ProcessAssemblies + /// in the outer build via filesystem fallback. /// [Output] public ITaskItem []? RewrittenAssemblies { get; set; } @@ -166,7 +168,7 @@ void ProcessMarshalMethods (AndroidTargetArch targetArch, string abi, AndroidRun ReportStatistics (targetArch, classifier); - // Step 3: Build output items for rewritten assemblies (DLLs and PDBs) + // Step 3: Build output items for rewritten assemblies BuildRewrittenAssembliesOutput (rewrittenOriginalPaths); // Step 4: Build NativeCodeGenStateObject and generate .ll @@ -195,9 +197,15 @@ HashSet RewriteAssemblies (AndroidTargetArch targetArch, MarshalMethodsC } /// - /// Build output items for assemblies (and PDBs) that were rewritten to the output directory. + /// Build output items for assemblies 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. + /// + /// PDB files are NOT included as output items — they are written alongside the rewritten + /// DLLs and will be discovered by ProcessAssemblies in the outer build via its + /// filesystem fallback (GetOrCreateSymbolItem). Reference PDBs are typically + /// not present in @(ResolvedFileToPublish), so adding them here would introduce + /// items the SDK conflict resolution doesn't expect. /// void BuildRewrittenAssembliesOutput (HashSet rewrittenOriginalPaths) { @@ -214,21 +222,10 @@ void BuildRewrittenAssembliesOutput (HashSet rewrittenOriginalPaths) 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 (); From 853fdaf41ed76a88376da686ef0a4230dc56655d Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Tue, 14 Apr 2026 10:28:36 -0700 Subject: [PATCH 5/6] Fix resource leak and missing lookup info in RewriteMarshalMethods Address three bugs introduced in the marshal methods pipeline move: 1. Wrap XAAssemblyResolver in try/finally so it is always disposed, even when classification, rewriting, or LLVM IR generation throws. 2. Thread ManagedMarshalMethodsLookupInfo through to CreateNativeCodeGenStateObjectFromClassifier so that when EnableManagedMarshalMethodsLookup is true, the NativeCallback indices (AssemblyIndex/ClassIndex/MethodIndex) are populated before LLVM IR generation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tasks/RewriteMarshalMethods.cs | 55 ++++++++++--------- .../Utilities/MarshalMethodCecilAdapter.cs | 4 +- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs index 3605093d70f..2c94c154faf 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs @@ -145,38 +145,39 @@ void ProcessMarshalMethods (AndroidTargetArch targetArch, string abi, AndroidRun var assemblyItems = assemblyDict.Values.ToList (); XAAssemblyResolver resolver = MonoAndroidHelper.MakeResolver (Log, useMarshalMethods: true, targetArch, assemblyDict); - - 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; - } - - // Step 2: Rewrite assemblies to the per-RID output directory - HashSet rewrittenOriginalPaths; - if (!EnableManagedMarshalMethodsLookup) { - rewrittenOriginalPaths = RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled); - classifier.AddSpecialCaseMethods (); - } else { - classifier.AddSpecialCaseMethods (); - var lookupInfo = new ManagedMarshalMethodsLookupInfo (Log); - rewrittenOriginalPaths = RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled, lookupInfo); - } + 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; + } - ReportStatistics (targetArch, classifier); + // Step 2: Rewrite assemblies to the per-RID output directory + ManagedMarshalMethodsLookupInfo? lookupInfo = null; + HashSet rewrittenOriginalPaths; + if (!EnableManagedMarshalMethodsLookup) { + rewrittenOriginalPaths = RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled); + classifier.AddSpecialCaseMethods (); + } else { + classifier.AddSpecialCaseMethods (); + lookupInfo = new ManagedMarshalMethodsLookupInfo (Log); + rewrittenOriginalPaths = RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled, lookupInfo); + } - // Step 3: Build output items for rewritten assemblies - BuildRewrittenAssembliesOutput (rewrittenOriginalPaths); + ReportStatistics (targetArch, classifier); - // Step 4: Build NativeCodeGenStateObject and generate .ll - var codeGenState = MarshalMethodCecilAdapter.CreateNativeCodeGenStateObjectFromClassifier (targetArch, classifier); - GenerateLlvmIr (targetArch, abi, androidRuntime, codeGenState); + // Step 3: Build output items for rewritten assemblies + BuildRewrittenAssembliesOutput (rewrittenOriginalPaths); - // Step 5: Dispose Cecil resolvers - resolver.Dispose (); + // Step 4: Build NativeCodeGenStateObject and generate .ll + var codeGenState = MarshalMethodCecilAdapter.CreateNativeCodeGenStateObjectFromClassifier (targetArch, classifier, lookupInfo); + GenerateLlvmIr (targetArch, abi, androidRuntime, codeGenState); + } finally { + resolver.Dispose (); + } } /// diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs index 1d50aeb7720..5228a7c2eb4 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs @@ -77,7 +77,7 @@ static NativeCodeGenStateObject CreateNativeCodeGenState (AndroidTargetArch arch /// JavaTypesForJCW, TypeCache, etc.) is not available. Only populates marshal methods /// data needed for LLVM IR generation. /// - public static NativeCodeGenStateObject CreateNativeCodeGenStateObjectFromClassifier (AndroidTargetArch arch, MarshalMethodsCollection classifier) + public static NativeCodeGenStateObject CreateNativeCodeGenStateObjectFromClassifier (AndroidTargetArch arch, MarshalMethodsCollection classifier, ManagedMarshalMethodsLookupInfo? lookupInfo = null) { var obj = new NativeCodeGenStateObject { TargetArch = arch, @@ -87,7 +87,7 @@ public static NativeCodeGenStateObject CreateNativeCodeGenStateObjectFromClassif var methods = new List (group.Value.Count); foreach (var method in group.Value) { - var entry = CreateEntry (method, info: null); + var entry = CreateEntry (method, lookupInfo); methods.Add (entry); } From 5c4ad1f6c8f089451d54bbf88732331122aedf3e Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Tue, 14 Apr 2026 14:24:29 -0700 Subject: [PATCH 6/6] Generate ApplicationRegistration.java in inner build when marshal methods are enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When marshal methods are enabled, Application/Instrumentation types that have no dynamically registered methods should be excluded from ApplicationRegistration.java — their JCWs no longer have __md_methods. Previously, the classifier in GenerateJavaStubs (outer build) filtered these types. The marshal-methods-v2 PR moved classification to the inner build, but left ApplicationRegistration.java generation in the outer build without a classifier, causing javac errors. Fix: generate ApplicationRegistration.java in RewriteMarshalMethods (inner build) where the classifier is available. Each inner build writes to a per-RID directory; the target copies it into the shared Java source tree. When marshal methods are disabled, the outer build's GenerateAdditionalProviderSources still generates it as before. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- pr-message.md | 7 ++ run-test-similar-androidx.sh | 25 +++++ run-test.sh | 18 ++++ ...crosoft.Android.Sdk.TypeMap.LlvmIr.targets | 11 ++ .../GenerateAdditionalProviderSources.cs | 54 ++++++---- .../Tasks/RewriteMarshalMethods.cs | 102 ++++++++++++++++++ 6 files changed, 196 insertions(+), 21 deletions(-) create mode 100644 pr-message.md create mode 100755 run-test-similar-androidx.sh create mode 100755 run-test.sh diff --git a/pr-message.md b/pr-message.md new file mode 100644 index 00000000000..c3ed222945b --- /dev/null +++ b/pr-message.md @@ -0,0 +1,7 @@ +[marshal methods] Move marshal method pipeline from outer build to inner (per-RID) build + +## Summary + +Moves marshal method classification, assembly rewriting, and `.ll` LLVM IR generation from the outer build into the inner (per-RID) build's `RewriteMarshalMethods` task, running after ILLink trimming on already-trimmed assemblies. This eliminates the token staleness problem where classification captured token state, then rewriting mutated tokens, then downstream tasks saw stale data. + +When marshal methods are enabled, the task classifies, rewrites assemblies, and generates a full `.ll`. When disabled, it generates an empty/minimal `.ll` with just the structural scaffolding the native runtime links against. `GenerateNativeMarshalMethodSources` is stripped down to P/Invoke preservation only. diff --git a/run-test-similar-androidx.sh b/run-test-similar-androidx.sh new file mode 100755 index 00000000000..8ba8150f6aa --- /dev/null +++ b/run-test-similar-androidx.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Wrapper script to run the SimilarAndroidXAssemblyNames(False,MonoVM) test case locally. +# Usage: ./run-test-similar-androidx.sh [Configuration] +# Configuration defaults to "Release" + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONFIG="${1:-Release}" +TFM="net10.0" +DLL="$SCRIPT_DIR/bin/Test${CONFIG}/${TFM}/Xamarin.Android.Build.Tests.dll" + +if [[ ! -f "$DLL" ]]; then + echo "ERROR: Test DLL not found at: $DLL" + echo " You may need to build the tests first, or pass a different Configuration." + exit 1 +fi + +echo "=== Running: SimilarAndroidXAssemblyNames(False,MonoVM) ===" +echo " DLL: $DLL" +echo "" + +exec "$SCRIPT_DIR/dotnet-local.sh" test "$DLL" \ + --logger "console;verbosity=detailed" \ + -- NUnit.Where="method == SimilarAndroidXAssemblyNames and test =~ /False,MonoVM/" diff --git a/run-test.sh b/run-test.sh new file mode 100755 index 00000000000..002b2efdc4c --- /dev/null +++ b/run-test.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Run the CustomApplicationClassAndMultiDex(True,MonoVM) test locally +# This test failed in CI with a build failure related to marshal methods pipeline changes. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")" && pwd)" +TEST_DLL="$REPO_ROOT/bin/TestRelease/net10.0/Xamarin.Android.Build.Tests.dll" + +if [ ! -f "$TEST_DLL" ]; then + echo "ERROR: Test DLL not found at $TEST_DLL" + echo "You may need to build the tests first." + exit 1 +fi + +echo "Running: CustomApplicationClassAndMultiDex(True,MonoVM)" +exec "$REPO_ROOT/dotnet-local.sh" test "$TEST_DLL" \ + --filter "Name~CustomApplicationClassAndMultiDex" \ + -- NUnit.NumberOfTestWorkers=1 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 9a1cf6cd561..7da5927b5f7 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 @@ -130,6 +130,7 @@ + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateAdditionalProviderSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateAdditionalProviderSources.cs index 36effd2b068..ee6a67a774c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateAdditionalProviderSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateAdditionalProviderSources.cs @@ -37,6 +37,13 @@ public class GenerateAdditionalProviderSources : AndroidTask public string? HttpClientHandlerType { get; set; } public bool EnableSGenConcurrent { get; set; } + /// + /// When true, ApplicationRegistration.java is generated by the inner build's + /// task (which has the classifier to filter types). + /// This task skips generating it to avoid overwriting the inner build's output. + /// + public bool EnableMarshalMethods { get; set; } + AndroidRuntime androidRuntime; JavaPeerStyle codeGenerationTarget; @@ -114,29 +121,34 @@ void Generate (NativeCodeGenStateObject codeGenState) } // Create additional application java sources. - StringWriter regCallsWriter = new StringWriter (); - regCallsWriter.WriteLine ("// Application and Instrumentation ACWs must be registered first."); - - foreach ((string jniName, string assemblyQualifiedName) in codeGenState.ApplicationsAndInstrumentationsToRegister) { - regCallsWriter.WriteLine ( - codeGenerationTarget == JavaPeerStyle.XAJavaInterop1 ? - "\t\tmono.android.Runtime.register (\"{0}\", {1}.class, {1}.__md_methods);" : - "\t\tnet.dot.jni.ManagedPeer.registerNativeMembers ({1}.class, {1}.__md_methods);", - assemblyQualifiedName, - jniName - ); - } + // When marshal methods are enabled, ApplicationRegistration.java is generated + // by RewriteMarshalMethods in the inner build, which has the classifier to + // filter out types with no dynamically registered methods. + if (!EnableMarshalMethods) { + StringWriter regCallsWriter = new StringWriter (); + regCallsWriter.WriteLine ("// Application and Instrumentation ACWs must be registered first."); + + foreach ((string jniName, string assemblyQualifiedName) in codeGenState.ApplicationsAndInstrumentationsToRegister) { + regCallsWriter.WriteLine ( + codeGenerationTarget == JavaPeerStyle.XAJavaInterop1 ? + "\t\tmono.android.Runtime.register (\"{0}\", {1}.class, {1}.__md_methods);" : + "\t\tnet.dot.jni.ManagedPeer.registerNativeMembers ({1}.class, {1}.__md_methods);", + assemblyQualifiedName, + jniName + ); + } - regCallsWriter.Close (); + regCallsWriter.Close (); - var real_app_dir = Path.Combine (OutputDirectory, "src", "net", "dot", "android"); - string applicationTemplateFile = "ApplicationRegistration.java"; - SaveResource ( - applicationTemplateFile, - applicationTemplateFile, - real_app_dir, - template => template.Replace ("// REGISTER_APPLICATION_AND_INSTRUMENTATION_CLASSES_HERE", regCallsWriter.ToString ()) - ); + var real_app_dir = Path.Combine (OutputDirectory, "src", "net", "dot", "android"); + string applicationTemplateFile = "ApplicationRegistration.java"; + SaveResource ( + applicationTemplateFile, + applicationTemplateFile, + real_app_dir, + template => template.Replace ("// REGISTER_APPLICATION_AND_INSTRUMENTATION_CLASSES_HERE", regCallsWriter.ToString ()) + ); + } void AppendEnvVarEntry (StringBuilder sb, string value) { diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs index 2c94c154faf..d89c8a784aa 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs @@ -3,9 +3,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; +using Java.Interop.Tools.Cecil; +using Java.Interop.Tools.JavaCallableWrappers; +using Java.Interop.Tools.TypeNameMappings; using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using Mono.Cecil; using Xamarin.Android.Tools; namespace Xamarin.Android.Tasks; @@ -62,6 +67,12 @@ public class RewriteMarshalMethods : AndroidTask /// public bool EnableMarshalMethods { get; set; } + /// + /// Code generation target style (e.g. XAJavaInterop1). Determines which Java API + /// is used in the ApplicationRegistration.java register calls. + /// + public string CodeGenerationTarget { get; set; } = ""; + /// /// Environment files to parse for configuration (e.g. XA_BROKEN_EXCEPTION_TRANSITIONS). /// Only used when marshal methods are enabled. @@ -86,6 +97,14 @@ public class RewriteMarshalMethods : AndroidTask [Required] public string RewrittenAssembliesOutputDirectory { get; set; } = ""; + /// + /// Per-RID output directory for ApplicationRegistration.java. + /// Each inner build writes its own copy here; the outer build picks one + /// deterministic RID's file and copies it into the shared Java source tree. + /// + [Required] + public string ApplicationRegistrationOutputDirectory { get; set; } = ""; + /// /// The RuntimeIdentifier for this inner build (e.g. android-arm64). /// Converted to an ABI and target architecture internally. @@ -106,6 +125,17 @@ public class RewriteMarshalMethods : AndroidTask [Output] public ITaskItem []? RewrittenAssemblies { get; set; } + /// + /// Output: path to the ApplicationRegistration.java file generated + /// by this inner build. When marshal methods are enabled, Application and + /// Instrumentation types that have no dynamically registered methods are + /// excluded from the registration list. The outer build copies one RID's + /// file into the shared Java source tree. When marshal methods are disabled, + /// this output is empty and the outer build generates the file itself. + /// + [Output] + public string? ApplicationRegistrationJavaFile { get; set; } + public override bool RunTask () { var androidRuntime = MonoAndroidHelper.ParseAndroidRuntime (AndroidRuntime); @@ -175,6 +205,9 @@ void ProcessMarshalMethods (AndroidTargetArch targetArch, string abi, AndroidRun // Step 4: Build NativeCodeGenStateObject and generate .ll var codeGenState = MarshalMethodCecilAdapter.CreateNativeCodeGenStateObjectFromClassifier (targetArch, classifier, lookupInfo); GenerateLlvmIr (targetArch, abi, androidRuntime, codeGenState); + + // Step 5: Generate ApplicationRegistration.java with classifier-filtered types + GenerateApplicationRegistration (classifier, resolver, assemblyDict); } finally { resolver.Dispose (); } @@ -329,4 +362,73 @@ void EnsureAbiMetadata (string abi) return (assemblyCount, uniqueAssemblyNames); } + + /// + /// Generates ApplicationRegistration.java in the per-RID output directory. + /// When marshal methods are enabled, Application and Instrumentation types that have + /// no dynamically registered methods are excluded — their JCWs no longer have the + /// __md_methods field, so referencing it from ApplicationRegistration.java + /// would cause a javac error. + /// + void GenerateApplicationRegistration (MarshalMethodsCollection classifier, XAAssemblyResolver resolver, Dictionary assemblyDict) + { + var codeGenerationTarget = MonoAndroidHelper.ParseCodeGenerationTarget (CodeGenerationTarget); + var tdCache = new TypeDefinitionCache (); + + var regCallsWriter = new StringWriter (); + regCallsWriter.WriteLine ("\t\t// Application and Instrumentation ACWs must be registered first."); + + foreach (var kvp in assemblyDict) { + var assemblyDef = resolver.Resolve (AssemblyNameReference.Parse (kvp.Key)); + if (assemblyDef is null) { + continue; + } + + foreach (var module in assemblyDef.Modules) { + foreach (var type in module.Types) { + if (!JavaNativeTypeManager.IsApplication (type, tdCache) && !JavaNativeTypeManager.IsInstrumentation (type, tdCache)) { + continue; + } + + if (!classifier.TypeHasDynamicallyRegisteredMethods (type)) { + Log.LogDebugMessage ($"Skipping Application/Instrumentation type '{type.FullName}' from ApplicationRegistration — no dynamically registered methods"); + continue; + } + + var jniName = JavaNativeTypeManager.ToJniName (type, tdCache).Replace ('/', '.'); + var assemblyQualifiedName = type.GetAssemblyQualifiedName (tdCache); + + regCallsWriter.WriteLine ( + codeGenerationTarget == JavaPeerStyle.XAJavaInterop1 ? + "\t\tmono.android.Runtime.register (\"{0}\", {1}.class, {1}.__md_methods);" : + "\t\tnet.dot.jni.ManagedPeer.registerNativeMembers ({1}.class, {1}.__md_methods);", + assemblyQualifiedName, + jniName + ); + } + } + } + + regCallsWriter.Close (); + + string template = GetApplicationRegistrationTemplate (); + string content = template.Replace ("// REGISTER_APPLICATION_AND_INSTRUMENTATION_CLASSES_HERE", regCallsWriter.ToString ()); + + Directory.CreateDirectory (ApplicationRegistrationOutputDirectory); + + string outputPath = Path.Combine (ApplicationRegistrationOutputDirectory, "ApplicationRegistration.java"); + Files.CopyIfStringChanged (content, outputPath); + ApplicationRegistrationJavaFile = outputPath; + Log.LogDebugMessage ($"Generated ApplicationRegistration.java: {outputPath}"); + } + + static string GetApplicationRegistrationTemplate () + { + using var stream = typeof (RewriteMarshalMethods).Assembly.GetManifestResourceStream ("ApplicationRegistration.java"); + if (stream is null) { + throw new InvalidOperationException ("Could not find embedded resource 'ApplicationRegistration.java'"); + } + using var reader = new StreamReader (stream); + return reader.ReadToEnd (); + } }