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.