Skip to content

Commit ea399ed

Browse files
authored
[XABT] Scan for JLO needed wrappers in LinkAssembliesNoShrink. (#9893)
Today, application builds use Cecil to scan through all Android assemblies at least twice: - Debug: `_LinkAssembliesNoShrink`, `_GenerateJavaStubs` - Release: `_ILLink`, `_GenerateJavaStubs`, `_RemoveRegisterAttribute` This is a costly operation that we would like to only perform once to help speed up application builds. The long-term goal is to move the (many) steps currently performed by `_GenerateJavaStubs` into "linker" steps. This PR moves the Cecil scanning required for the first step: "generating Java Callable Wrappers". ## Implementation This does not move generating JCW Java code to the linker, it only moves scanning for the JLO information needed to generate JCWs to the linker. This information is persisted as an XML file in `/obj` next to the processed assembly. Example: - `obj/Debug/net10.0-android/android/assets/MyApplication.dll` - `obj/Debug/net10.0-android/android/assets/MyApplication.jlo.xml` Doing it this way has two benefits: it helps keep the linker from getting too complex (eg: generating Java code) and it helps incremental builds. If an assembly has not changed since the last build, the JLO information in the `.jlo.xml` is still valid and will be reused. The assembly does not need to be re-scanned. (For example, the ~70 AndroidX assemblies that the MAUI template uses do not need to be re-scanned on every Debug build.) The process of actually generating the JCWs is done by a new `GenerateJavaCallableWrappers` task in the `_GenerateJavaStubs` target that consumes the `.jlo.xml` files and outputs the `.java` files. ## Release Builds In an ideal Release build, we would (probably?) run this new step as part of the `ILLink` invocation. However there are some tricky limitations to running in the linker pipeline, chiefly being that the linker cannot load external assemblies like `Java.Interop.Tool.JavaCallableWrappers.dll` (and dependencies) that we need. This can be worked around by including the needed source files in the linker assembly, but this will require additional work. Instead, for now, we will add an invocation a new `_LinkAssembliesAdditionalSteps` target to linked builds that runs after `ILLink`. This calls the existing `LinkAssembliesNoShrink` task to scan for JLOs but does not execute the other linker tasks that have already been run. This allows us to move forward with converting to "linker steps" for making Debug builds faster without the extra work required to move these steps into `ILLink`. A future enhancement can perform this work if desired. (There is also [some question](dotnet/runtime#107211) as to whether we actually want to create new `ILLink` linker steps.) Note this temporarily leaves the `$(_AndroidJLOCheckedBuild)` flag that compares the generated Java files to the previous ones. This will likely be useful ensuring future moved steps are correct as well.
1 parent 60cecaa commit ea399ed

13 files changed

+668
-135
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#nullable enable
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using Java.Interop.Tools.JavaCallableWrappers;
6+
using Java.Interop.Tools.JavaCallableWrappers.Adapters;
7+
using Java.Interop.Tools.JavaCallableWrappers.CallableWrapperMembers;
8+
using Microsoft.Android.Build.Tasks;
9+
using Microsoft.Build.Utilities;
10+
using Mono.Cecil;
11+
using Mono.Linker;
12+
using Mono.Linker.Steps;
13+
using Xamarin.Android.Tasks;
14+
15+
namespace MonoDroid.Tuner;
16+
17+
/// <summary>
18+
/// Scans an assembly for JLOs that need JCWs generated and writes them to an XML file.
19+
/// </summary>
20+
public class FindJavaObjectsStep : BaseStep
21+
{
22+
public string ApplicationJavaClass { get; set; } = "";
23+
24+
public bool ErrorOnCustomJavaObject { get; set; }
25+
26+
public bool UseMarshalMethods { get; set; }
27+
28+
public TaskLoggingHelper Log { get; set; }
29+
30+
public FindJavaObjectsStep (TaskLoggingHelper log) => Log = log;
31+
32+
public bool ProcessAssembly (AssemblyDefinition assembly, string destinationJLOXml)
33+
{
34+
var action = Annotations.HasAction (assembly) ? Annotations.GetAction (assembly) : AssemblyAction.Skip;
35+
36+
if (action == AssemblyAction.Delete)
37+
return false;
38+
39+
var types = ScanForJavaTypes (assembly);
40+
var initial_count = types.Count;
41+
42+
// Filter out Java types we don't care about
43+
types = types.Where (t => !t.IsInterface && !JavaTypeScanner.ShouldSkipJavaCallableWrapperGeneration (t, Context)).ToList ();
44+
45+
Log.LogDebugMessage ($"{assembly.Name.Name} - Found {initial_count} Java types, filtered to {types.Count}");
46+
47+
var wrappers = ConvertToCallableWrappers (types);
48+
49+
using (var sw = MemoryStreamPool.Shared.CreateStreamWriter ()) {
50+
XmlExporter.Export (sw, wrappers, true);
51+
Files.CopyIfStreamChanged (sw.BaseStream, destinationJLOXml);
52+
}
53+
54+
return true;
55+
}
56+
57+
public static void WriteEmptyXmlFile (string destination)
58+
{
59+
XmlExporter.Export (destination, [], false);
60+
}
61+
62+
List<TypeDefinition> ScanForJavaTypes (AssemblyDefinition assembly)
63+
{
64+
var types = new List<TypeDefinition> ();
65+
66+
var scanner = new XAJavaTypeScanner (Xamarin.Android.Tools.AndroidTargetArch.None, Log, Context) {
67+
ErrorOnCustomJavaObject = ErrorOnCustomJavaObject
68+
};
69+
70+
foreach (ModuleDefinition md in assembly.Modules) {
71+
foreach (TypeDefinition td in md.Types) {
72+
scanner.AddJavaType (td, types);
73+
}
74+
}
75+
76+
return types;
77+
}
78+
79+
List<CallableWrapperType> ConvertToCallableWrappers (List<TypeDefinition> types)
80+
{
81+
var wrappers = new List<CallableWrapperType> ();
82+
83+
var reader_options = new CallableWrapperReaderOptions {
84+
DefaultApplicationJavaClass = ApplicationJavaClass,
85+
DefaultMonoRuntimeInitialization = "mono.MonoPackageManager.LoadApplication (context);",
86+
};
87+
88+
if (UseMarshalMethods)
89+
reader_options.MethodClassifier = new MarshalMethodsClassifier (Context, Context.Resolver, Log);
90+
91+
foreach (var type in types) {
92+
var wrapper = CecilImporter.CreateType (type, Context, reader_options);
93+
wrappers.Add (wrapper);
94+
}
95+
96+
return wrappers;
97+
}
98+
}

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ _ResolveAssemblies MSBuild target.
198198
_ResolveSatellitePaths;
199199
_CreatePackageWorkspace;
200200
_LinkAssemblies;
201+
_AfterILLinkAdditionalSteps;
201202
</_PrepareAssembliesDependsOnTargets>
202203
</PropertyGroup>
203204

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.BuildOrder.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ properties that determine build ordering.
6767
_ResolveSatellitePaths;
6868
_CreatePackageWorkspace;
6969
_LinkAssemblies;
70+
_AfterILLinkAdditionalSteps;
7071
_GenerateJavaStubs;
7172
_ManifestMerger;
7273
_ConvertCustomView;
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
#nullable enable
2+
using System;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
using System.Linq;
6+
using Java.Interop.Tools.Cecil;
7+
using Java.Interop.Tools.JavaCallableWrappers;
8+
using Java.Interop.Tools.TypeNameMappings;
9+
using Microsoft.Android.Build.Tasks;
10+
using Microsoft.Build.Framework;
11+
using Mono.Cecil;
12+
using MonoDroid.Tuner;
13+
using Xamarin.Android.Tools;
14+
using PackageNamingPolicyEnum = Java.Interop.Tools.TypeNameMappings.PackageNamingPolicy;
15+
16+
namespace Xamarin.Android.Tasks;
17+
18+
/// <summary>
19+
/// This task runs additional "linker steps" that are not part of ILLink. These steps
20+
/// are run *after* the linker has run. Additionally, this task is run by
21+
/// LinkAssembliesNoShrink to modify assemblies when ILLink is not used.
22+
/// </summary>
23+
public class AssemblyModifierPipeline : AndroidTask
24+
{
25+
// Names of assemblies which don't have Mono.Android.dll references, or are framework assemblies, but which must
26+
// be scanned for Java types.
27+
static readonly HashSet<string> SpecialAssemblies = new HashSet<string> (StringComparer.OrdinalIgnoreCase) {
28+
"Java.Interop.dll",
29+
"Mono.Android.dll",
30+
"Mono.Android.Runtime.dll",
31+
};
32+
33+
public override string TaskPrefix => "AMP";
34+
35+
public string ApplicationJavaClass { get; set; } = "";
36+
37+
public string CodeGenerationTarget { get; set; } = "";
38+
39+
public bool Debug { get; set; }
40+
41+
[Required]
42+
public ITaskItem [] DestinationFiles { get; set; } = [];
43+
44+
public bool Deterministic { get; set; }
45+
46+
public bool EnableMarshalMethods { get; set; }
47+
48+
public bool ErrorOnCustomJavaObject { get; set; }
49+
50+
public string? PackageNamingPolicy { get; set; }
51+
52+
/// <summary>
53+
/// Defaults to false, enables Mono.Cecil to load symbols
54+
/// </summary>
55+
public bool ReadSymbols { get; set; }
56+
57+
/// <summary>
58+
/// These are used so we have the full list of SearchDirectories
59+
/// </summary>
60+
[Required]
61+
public ITaskItem [] ResolvedAssemblies { get; set; } = [];
62+
63+
[Required]
64+
public ITaskItem [] ResolvedUserAssemblies { get; set; } = [];
65+
66+
[Required]
67+
public ITaskItem [] SourceFiles { get; set; } = [];
68+
69+
protected JavaPeerStyle codeGenerationTarget;
70+
71+
public override bool RunTask ()
72+
{
73+
codeGenerationTarget = MonoAndroidHelper.ParseCodeGenerationTarget (CodeGenerationTarget);
74+
JavaNativeTypeManager.PackageNamingPolicy = Enum.TryParse (PackageNamingPolicy, out PackageNamingPolicyEnum pnp) ? pnp : PackageNamingPolicyEnum.LowercaseCrc64;
75+
76+
if (SourceFiles.Length != DestinationFiles.Length)
77+
throw new ArgumentException ("source and destination count mismatch");
78+
79+
var readerParameters = new ReaderParameters {
80+
ReadSymbols = ReadSymbols,
81+
};
82+
83+
var writerParameters = new WriterParameters {
84+
DeterministicMvid = Deterministic,
85+
};
86+
87+
Dictionary<AndroidTargetArch, Dictionary<string, ITaskItem>> perArchAssemblies = MonoAndroidHelper.GetPerArchAssemblies (ResolvedAssemblies, Array.Empty<string> (), validate: false);
88+
89+
RunState? runState = null;
90+
var currentArch = AndroidTargetArch.None;
91+
92+
for (int i = 0; i < SourceFiles.Length; i++) {
93+
ITaskItem source = SourceFiles [i];
94+
AndroidTargetArch sourceArch = GetValidArchitecture (source);
95+
ITaskItem destination = DestinationFiles [i];
96+
AndroidTargetArch destinationArch = GetValidArchitecture (destination);
97+
98+
if (sourceArch != destinationArch) {
99+
throw new InvalidOperationException ($"Internal error: assembly '{sourceArch}' targets architecture '{sourceArch}', while destination assembly '{destination}' targets '{destinationArch}' instead");
100+
}
101+
102+
// Each architecture must have a different set of context classes, or otherwise only the first instance of the assembly may be rewritten.
103+
if (currentArch != sourceArch) {
104+
currentArch = sourceArch;
105+
runState?.Dispose ();
106+
107+
var resolver = new DirectoryAssemblyResolver (this.CreateTaskLogger (), loadDebugSymbols: ReadSymbols, loadReaderParameters: readerParameters);
108+
runState = new RunState (resolver);
109+
110+
// Add SearchDirectories for the current architecture's ResolvedAssemblies
111+
foreach (var kvp in perArchAssemblies [sourceArch]) {
112+
ITaskItem assembly = kvp.Value;
113+
var path = Path.GetFullPath (Path.GetDirectoryName (assembly.ItemSpec));
114+
if (!runState.resolver.SearchDirectories.Contains (path)) {
115+
runState.resolver.SearchDirectories.Add (path);
116+
}
117+
}
118+
119+
// Set up the FixAbstractMethodsStep and AddKeepAlivesStep
120+
var context = new MSBuildLinkContext (runState.resolver, Log);
121+
122+
CreateRunState (runState, context);
123+
}
124+
125+
Directory.CreateDirectory (Path.GetDirectoryName (destination.ItemSpec));
126+
127+
RunPipeline (source, destination, runState!, writerParameters);
128+
}
129+
130+
runState?.Dispose ();
131+
132+
return !Log.HasLoggedErrors;
133+
}
134+
135+
protected virtual void CreateRunState (RunState runState, MSBuildLinkContext context)
136+
{
137+
var findJavaObjectsStep = new FindJavaObjectsStep (Log) {
138+
ApplicationJavaClass = ApplicationJavaClass,
139+
ErrorOnCustomJavaObject = ErrorOnCustomJavaObject,
140+
UseMarshalMethods = EnableMarshalMethods,
141+
};
142+
143+
findJavaObjectsStep.Initialize (context);
144+
145+
runState.findJavaObjectsStep = findJavaObjectsStep;
146+
}
147+
148+
protected virtual void RunPipeline (ITaskItem source, ITaskItem destination, RunState runState, WriterParameters writerParameters)
149+
{
150+
var destinationJLOXml = Path.ChangeExtension (destination.ItemSpec, ".jlo.xml");
151+
152+
if (!TryScanForJavaObjects (source, destination, runState, writerParameters)) {
153+
// Even if we didn't scan for Java objects, we still write an empty .xml file for later steps
154+
FindJavaObjectsStep.WriteEmptyXmlFile (destinationJLOXml);
155+
}
156+
}
157+
158+
bool TryScanForJavaObjects (ITaskItem source, ITaskItem destination, RunState runState, WriterParameters writerParameters)
159+
{
160+
if (!ShouldScanAssembly (source))
161+
return false;
162+
163+
var destinationJLOXml = Path.ChangeExtension (destination.ItemSpec, ".jlo.xml");
164+
var assemblyDefinition = runState.resolver!.GetAssembly (source.ItemSpec);
165+
166+
var scanned = runState.findJavaObjectsStep!.ProcessAssembly (assemblyDefinition, destinationJLOXml);
167+
168+
return scanned;
169+
}
170+
171+
bool ShouldScanAssembly (ITaskItem source)
172+
{
173+
// Skip this assembly if it is not an Android assembly
174+
if (!IsAndroidAssembly (source)) {
175+
Log.LogDebugMessage ($"Skipping assembly '{source.ItemSpec}' because it is not an Android assembly");
176+
return false;
177+
}
178+
179+
// When marshal methods or non-JavaPeerStyle.XAJavaInterop1 are in use we do not want to skip non-user assemblies (such as Mono.Android) - we need to generate JCWs for them during
180+
// application build, unlike in Debug configuration or when marshal methods are disabled, in which case we use JCWs generated during Xamarin.Android
181+
// build and stored in a jar file.
182+
var useMarshalMethods = !Debug && EnableMarshalMethods;
183+
var shouldSkipNonUserAssemblies = !useMarshalMethods && codeGenerationTarget == JavaPeerStyle.XAJavaInterop1;
184+
185+
if (shouldSkipNonUserAssemblies && !ResolvedUserAssemblies.Any (a => a.ItemSpec == source.ItemSpec)) {
186+
Log.LogDebugMessage ($"Skipping assembly '{source.ItemSpec}' because it is not a user assembly and we don't need JLOs from non-user assemblies");
187+
return false;
188+
}
189+
190+
return true;
191+
}
192+
193+
bool IsAndroidAssembly (ITaskItem source)
194+
{
195+
string name = Path.GetFileName (source.ItemSpec);
196+
197+
if (SpecialAssemblies.Contains (name))
198+
return true;
199+
200+
return MonoAndroidHelper.IsMonoAndroidAssembly (source);
201+
}
202+
203+
AndroidTargetArch GetValidArchitecture (ITaskItem item)
204+
{
205+
AndroidTargetArch ret = MonoAndroidHelper.GetTargetArch (item);
206+
if (ret == AndroidTargetArch.None) {
207+
throw new InvalidOperationException ($"Internal error: assembly '{item}' doesn't target any architecture.");
208+
}
209+
210+
return ret;
211+
}
212+
213+
protected sealed class RunState : IDisposable
214+
{
215+
public DirectoryAssemblyResolver resolver;
216+
public FixAbstractMethodsStep? fixAbstractMethodsStep = null;
217+
public AddKeepAlivesStep? addKeepAliveStep = null;
218+
public FixLegacyResourceDesignerStep? fixLegacyResourceDesignerStep = null;
219+
public FindJavaObjectsStep? findJavaObjectsStep = null;
220+
bool disposed_value;
221+
222+
public RunState (DirectoryAssemblyResolver resolver)
223+
{
224+
this.resolver = resolver;
225+
}
226+
227+
private void Dispose (bool disposing)
228+
{
229+
if (!disposed_value) {
230+
if (disposing) {
231+
resolver?.Dispose ();
232+
fixAbstractMethodsStep = null;
233+
fixLegacyResourceDesignerStep = null;
234+
addKeepAliveStep = null;
235+
findJavaObjectsStep = null;
236+
}
237+
disposed_value = true;
238+
}
239+
}
240+
241+
public void Dispose ()
242+
{
243+
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
244+
Dispose (disposing: true);
245+
GC.SuppressFinalize (this);
246+
}
247+
}
248+
}

0 commit comments

Comments
 (0)