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 aa145ccb839..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 @@ -78,13 +78,6 @@ EnableNativeRuntimeLinking="$(_AndroidEnableNativeRuntimeLinking)"> - - - + + + + <_MarshalMethodsAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " /> + + + + + + + + + + + + + + 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/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..0c63d6aaa1e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeMarshalMethodSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeMarshalMethodSources.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; using Xamarin.Android.Tools; @@ -10,29 +9,15 @@ namespace Xamarin.Android.Tasks; /// -/// 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. + // P/Invoke preservation needs the state when native runtime linking is enabled. + var nativeCodeGenStates = BuildEngine4.UnregisterTaskObjectAssemblyLocal ( + MonoAndroidHelper.GetProjectBuildSpecificTaskObjectKey (GenerateJavaStubs.NativeCodeGenStateObjectRegisterTaskKey, WorkingDirectory, IntermediateOutputDirectory), + RegisteredTaskObjectLifetime.Build + ); - // 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 - ); - } - - // Generate native code for each supported ABI foreach (var abi in SupportedAbis) Generate (nativeCodeGenStates, abi); @@ -150,200 +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; - 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; - 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); - } - } + if (!EnableNativeRuntimeLinking) { + return; } - // 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); - } - } - - /// - /// 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 - ); - } - } - - /// - /// 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); + MonoAndroidHelper.LogTextStreamContents (Log, $"Partial contents of file '{pinvokePreserveLlFilePath}'", pinvokePreserveWriter.BaseStream); } } - - 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/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..d89c8a784aa 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/RewriteMarshalMethods.cs @@ -1,162 +1,434 @@ #nullable enable -using System.Collections.Concurrent; +using System; +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; /// -/// 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 runs in the inner (per-RID) build to generate the +/// marshal_methods.{abi}.ll LLVM IR file. +/// +/// 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. +/// +/// 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. /// -/// -/// 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 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. When false, the task skips classification + /// and rewriting and generates an empty/minimal .ll file. + /// + 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. /// 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's + /// _CompileNativeAssemblySources can compile it. + /// + [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; } = ""; + + /// + /// 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 IntermediateOutputDirectory { 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. - /// - /// - /// 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. - /// + public string ApplicationRegistrationOutputDirectory { get; set; } = ""; + + /// + /// The RuntimeIdentifier for this inner build (e.g. android-arm64). + /// Converted to an ABI and target architecture internally. + /// + [Required] + public string RuntimeIdentifier { get; set; } = ""; + + /// + /// 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. 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; } + + /// + /// 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 () { - // 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. + var androidRuntime = MonoAndroidHelper.ParseAndroidRuntime (AndroidRuntime); + + 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 { + GenerateEmptyLlvmIr (targetArch, abi, androidRuntime); + } + + return !Log.HasLoggedErrors; + } + + /// + /// Marshal methods enabled path: classify, rewrite assemblies to output directory, 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); - // Process each target architecture - foreach (var kvp in nativeCodeGenStates) { - NativeCodeGenState state = kvp.Value; + // Step 1: Open assemblies with Cecil and classify marshal methods + 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 (); - if (state.Classifier is null) { - Log.LogError ("state.Classifier cannot be null if marshal methods are enabled"); - return false; + XAAssemblyResolver resolver = MonoAndroidHelper.MakeResolver (Log, useMarshalMethods: true, targetArch, assemblyDict); + try { + 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; } - // Handle the ordering dependency between special case methods and managed lookup tables + // Step 2: Rewrite assemblies to the per-RID output directory + ManagedMarshalMethodsLookupInfo? lookupInfo = null; + HashSet rewrittenOriginalPaths; if (!EnableManagedMarshalMethodsLookup) { - // Standard path: rewrite first, then add special cases - RewriteMethods (state, brokenExceptionTransitionsEnabled); - state.Classifier.AddSpecialCaseMethods (); + rewrittenOriginalPaths = RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled); + classifier.AddSpecialCaseMethods (); } else { - // Managed lookup path: add special cases first so they appear in lookup tables - // We need to run `AddSpecialCaseMethods` before `RewriteMarshalMethods` so that we can see the special case - // methods (such as TypeManager.n_Activate_mm) when generating the managed lookup tables. - state.Classifier.AddSpecialCaseMethods (); - state.ManagedMarshalMethodsLookupInfo = new ManagedMarshalMethodsLookupInfo (Log); - RewriteMethods (state, brokenExceptionTransitionsEnabled); + classifier.AddSpecialCaseMethods (); + lookupInfo = new ManagedMarshalMethodsLookupInfo (Log); + rewrittenOriginalPaths = RewriteAssemblies (targetArch, classifier, resolver, brokenExceptionTransitionsEnabled, lookupInfo); } - // 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}"); + ReportStatistics (targetArch, classifier); + + // Step 3: Build output items for rewritten assemblies + BuildRewrittenAssembliesOutput (rewrittenOriginalPaths); + + // 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 (); + } + } + + /// + /// 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); + } + + HashSet RewriteAssemblies (AndroidTargetArch targetArch, MarshalMethodsCollection classifier, XAAssemblyResolver resolver, bool brokenExceptionTransitionsEnabled, ManagedMarshalMethodsLookupInfo? lookupInfo = null) + { + var rewriter = new MarshalMethodsAssemblyRewriter (Log, targetArch, classifier, resolver, lookupInfo); + return rewriter.Rewrite (brokenExceptionTransitionsEnabled, RewrittenAssembliesOutputDirectory); + } + + /// + /// 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) + { + if (rewrittenOriginalPaths.Count == 0) { + return; + } + + var rewrittenItems = new List (); + + foreach (var item in Assemblies) { + if (!rewrittenOriginalPaths.Contains (item.ItemSpec)) { + continue; } - // Count and report methods that need blittable workaround wrappers - var wrappedCount = state.Classifier.MarshalMethods.Sum (m => m.Value.Count (m2 => m2.NeedsBlittableWorkaround)); + string rewrittenPath = Path.Combine (RewrittenAssembliesOutputDirectory, Path.GetFileName (item.ItemSpec)); + + var dllItem = new TaskItem (rewrittenPath); + item.CopyMetadataTo (dllItem); + dllItem.SetMetadata ("OriginalItemSpec", item.ItemSpec); + rewrittenItems.Add (dllItem); + } + + RewrittenAssemblies = rewrittenItems.ToArray (); + } + + 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}"); + } - 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}"); + 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); } } + } - return !Log.HasLoggedErrors; + /// + /// 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; + 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); } /// - /// 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) + /// 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) { - if (state.Classifier == null) { - return; + 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 + ); + } + } } - var rewriter = new MarshalMethodsAssemblyRewriter (Log, state.TargetArch, state.Classifier, state.Resolver, state.ManagedMarshalMethodsLookupInfo); - rewriter.Rewrite (brokenExceptionTransitionsEnabled); + 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 (); } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MarshalMethodCecilAdapter.cs index 7c6119f5347..5228a7c2eb4 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, ManagedMarshalMethodsLookupInfo? lookupInfo = null) + { + 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, lookupInfo); + methods.Add (entry); + } + + obj.MarshalMethods.Add (group.Key, methods); + } + + return obj; + } + static MarshalMethodEntryObject CreateEntry (MarshalMethodEntry entry, ManagedMarshalMethodsLookupInfo? info) { var obj = new MarshalMethodEntryObject ( 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) { 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.