Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions pr-message.md
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 25 additions & 0 deletions run-test-similar-androidx.sh
Original file line number Diff line number Diff line change
@@ -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/"
18 changes: 18 additions & 0 deletions run-test.sh
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,6 @@
EnableNativeRuntimeLinking="$(_AndroidEnableNativeRuntimeLinking)">
</GenerateJavaStubs>

<RewriteMarshalMethods
Condition=" '$(_AndroidUseMarshalMethods)' == 'true' And '$(AndroidIncludeDebugSymbols)' == 'false' "
EnableManagedMarshalMethodsLookup="$(_AndroidUseManagedMarshalMethodsLookup)"
Environments="@(_EnvironmentFiles)"
IntermediateOutputDirectory="$(IntermediateOutputPath)">
</RewriteMarshalMethods>

<GenerateTypeMappings
AndroidRuntime="$(_AndroidRuntime)"
Debug="$(AndroidIncludeDebugSymbols)"
Expand Down Expand Up @@ -137,6 +130,7 @@
<GenerateAdditionalProviderSources
AdditionalProviderSources="@(_AdditionalProviderSources)"
AndroidRuntime="$(_AndroidRuntime)"
EnableMarshalMethods="$(_AndroidUseMarshalMethods)"
Environments="@(_EnvironmentFiles)"
HttpClientHandlerType="$(AndroidHttpClientHandlerType)"
EnableSGenConcurrent="$(AndroidEnableSGenConcurrent)"
Expand Down Expand Up @@ -250,6 +244,73 @@
Deterministic="$(Deterministic)" />
</Target>

<!--
Inner-build marshal method handling. Always runs (for every RID) to generate
the marshal_methods.<abi>.ll LLVM IR file.

When marshal methods are enabled (Release + MonoVM), the task also classifies
marshal methods, rewrites assemblies (adds [UnmanagedCallersOnly] wrappers,
removes connectors), and generates a full .ll with marshal method native
functions. Rewritten assemblies are always written to a separate per-RID
output directory (never in-place), so parallel inner builds don't conflict
even when the input assemblies point to a shared location (e.g. the NuGet
runtime pack when PublishTrimmed is false).

When marshal methods are disabled (Debug, or Release without marshal methods),
the task generates an empty/minimal .ll containing only the structural
scaffolding the native runtime always links against.

Runs AfterTargets="_PostTrimmingPipeline" which is itself AfterTargets="ILLink".
MSBuild fires AfterTargets hooks even when the referenced target is
condition-skipped, so this target runs in both trimmed and untrimmed builds.
When trimming IS active, _PostTrimmingPipeline completes first, ensuring
assemblies are trimmed before any rewrite. Runs before ReadyToRun/crossgen2
so that R2R images are generated from the rewritten assemblies.

The .ll file is written to $(_OuterIntermediateOutputPath)android/ so the
outer build's _CompileNativeAssemblySources can compile it.

Skipped for NativeAOT because it does not use .ll-based marshal methods
or native assembly sources (those targets are also NativeAOT-excluded).
-->
<Target Name="_RewriteMarshalMethodsInner"
AfterTargets="_PostTrimmingPipeline"
Condition=" '$(_AndroidRuntime)' != 'NativeAOT' ">
<ItemGroup>
<_MarshalMethodsAssembly Include="@(ResolvedFileToPublish)" Condition=" '%(Extension)' == '.dll' " />
</ItemGroup>
<RewriteMarshalMethods
Assemblies="@(_MarshalMethodsAssembly)"
CodeGenerationTarget="$(_AndroidJcwCodegenTarget)"
EnableManagedMarshalMethodsLookup="$(_AndroidUseManagedMarshalMethodsLookup)"
EnableMarshalMethods="$(_AndroidUseMarshalMethods)"
Environments="@(_EnvironmentFiles)"
MarshalMethodsOutputDirectory="$(_OuterIntermediateOutputPath)android"
RewrittenAssembliesOutputDirectory="$(IntermediateOutputPath)rewritten-marshal-methods"
ApplicationRegistrationOutputDirectory="$(IntermediateOutputPath)application-registration"
AndroidRuntime="$(_AndroidRuntime)"
RuntimeIdentifier="$(RuntimeIdentifier)">
<Output TaskParameter="RewrittenAssemblies" ItemName="_RewrittenMarshalMethodFile" />
<Output TaskParameter="ApplicationRegistrationJavaFile" PropertyName="_InnerBuildApplicationRegistrationJavaFile" />
</RewriteMarshalMethods>
<!-- Replace original DLL items in @(ResolvedFileToPublish) with the rewritten copies.
PDB files are written alongside the rewritten DLLs and are discovered by
ProcessAssemblies in the outer build via filesystem fallback.
The updated items flow back to the outer build via
_ComputeFilesToPublishForRuntimeIdentifiers' Returns attribute. -->
<ItemGroup Condition=" '@(_RewrittenMarshalMethodFile)' != '' ">
<ResolvedFileToPublish Remove="@(_RewrittenMarshalMethodFile->'%(OriginalItemSpec)')" />
<ResolvedFileToPublish Include="@(_RewrittenMarshalMethodFile)" />
</ItemGroup>
<!-- Copy ApplicationRegistration.java into the shared Java source tree so javac can find it.
All RIDs produce identical content; whichever inner build runs last wins (safe). -->
<Copy
Condition=" '$(_InnerBuildApplicationRegistrationJavaFile)' != '' "
SourceFiles="$(_InnerBuildApplicationRegistrationJavaFile)"
DestinationFiles="$(_OuterIntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java"
SkipUnchangedFiles="true" />
</Target>

<!-- Inject _TypeMapKind into the property cache -->
<Target Name="_SetTypemapProperties"
BeforeTargets="_CreatePropertiesCache">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ public class GenerateAdditionalProviderSources : AndroidTask
public string? HttpClientHandlerType { get; set; }
public bool EnableSGenConcurrent { get; set; }

/// <summary>
/// When true, <c>ApplicationRegistration.java</c> is generated by the inner build's
/// <see cref="RewriteMarshalMethods"/> task (which has the classifier to filter types).
/// This task skips generating it to avoid overwriting the inner build's output.
/// </summary>
public bool EnableMarshalMethods { get; set; }

AndroidRuntime androidRuntime;
JavaPeerStyle codeGenerationTarget;

Expand Down Expand Up @@ -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)
{
Expand Down
10 changes: 4 additions & 6 deletions src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,12 +244,10 @@ internal static Dictionary<string, ITaskItem> 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<TypeDefinition> allJavaTypes, List<TypeDefinition> javaTypesForJCW) ScanForJavaTypes (XAAssemblyResolver res, TypeDefinitionCache cache, Dictionary<string, ITaskItem> assemblies, Dictionary<string, ITaskItem> userAssemblies, bool useMarshalMethods)
Expand Down
Loading
Loading