Skip to content

Commit 186a9fc

Browse files
authored
[Xamarin.Android.Build.Tasks, monodroid] Marshal Method Classifier (#7123)
Context: e1af958 Context: #7163 Changes: dotnet/java-interop@c942ab6...fadbb82 * dotnet/java-interop@fadbb82c: [generator] Add support for @explicitInterface metadata (#1006) * dotnet/java-interop@3e4a3c4f: [Java.Interop.Tools.JavaCallableWrappers] JavaCallableMethodClassifier (#998) dotnet/java-interop@3e4a3c4f reworked how `Java.Interop.Tools.JavaCallableWrappers.JavaCallableWrapperGenerator` can be used to find JNI marshal methods. Update `Xamarin.Android.Build.Tasks.dll` to provide a `JavaCallableMethodClassifier` to `JavaCallableWrapperGenerator`, collecting Cecil `MethodDefinition` instances for JNI marshal methods which can be created at build time via LLVM Marshal Methods. Methods which cannot be created at build time include methods with `[Export]`. Once candidate LLVM marshal methods are found, the marshal method is updated to have the [`UnmanagedCallersOnlyAttribute` attribute][0], and related System.Reflection.Emit-based infrastructure such as the `Get…Handler()` methods and `cb_…` fields are removed. As with e1af958, this feature is *not* enabled by default, and remains a xamarin-android Build time configuration option. [0]:https://docs.microsoft.com/dotnet/api/system.runtime.interopservices.unmanagedcallersonlyattribute?view=net-6.0
1 parent 822667f commit 186a9fc

File tree

11 files changed

+658
-32
lines changed

11 files changed

+658
-32
lines changed

external/Java.Interop

src/Xamarin.Android.Build.Tasks/Tasks/GenerateJavaStubs.cs

Lines changed: 59 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,12 @@ void Run (DirectoryAssemblyResolver res)
124124
if (Directory.Exists (dir.ItemSpec))
125125
res.SearchDirectories.Add (dir.ItemSpec);
126126
}
127-
127+
#if ENABLE_MARSHAL_METHODS
128+
Dictionary<string, HashSet<string>> marshalMethodsAssemblyPaths = null;
129+
if (!Debug) {
130+
marshalMethodsAssemblyPaths = new Dictionary<string, HashSet<string>> (StringComparer.Ordinal);
131+
}
132+
#endif
128133
// Put every assembly we'll need in the resolver
129134
bool hasExportReference = false;
130135
bool haveMonoAndroid = false;
@@ -159,6 +164,11 @@ void Run (DirectoryAssemblyResolver res)
159164
}
160165

161166
res.Load (assembly.ItemSpec);
167+
#if ENABLE_MARSHAL_METHODS
168+
if (!Debug) {
169+
StoreMarshalAssemblyPath (Path.GetFileNameWithoutExtension (assembly.ItemSpec), assembly);
170+
}
171+
#endif
162172
}
163173

164174
// However we only want to look for JLO types in user code for Java stub code generation
@@ -172,6 +182,9 @@ void Run (DirectoryAssemblyResolver res)
172182
string name = Path.GetFileNameWithoutExtension (asm.ItemSpec);
173183
if (!userAssemblies.ContainsKey (name))
174184
userAssemblies.Add (name, asm.ItemSpec);
185+
#if ENABLE_MARSHAL_METHODS
186+
StoreMarshalAssemblyPath (name, asm);
187+
#endif
175188
}
176189

177190
// Step 1 - Find all the JLO types
@@ -182,10 +195,6 @@ void Run (DirectoryAssemblyResolver res)
182195

183196
List<TypeDefinition> allJavaTypes = scanner.GetJavaTypes (allTypemapAssemblies, res);
184197

185-
// Step 2 - Generate type maps
186-
// Type mappings need to use all the assemblies, always.
187-
WriteTypeMappings (allJavaTypes, cache);
188-
189198
var javaTypes = new List<TypeDefinition> ();
190199
foreach (TypeDefinition td in allJavaTypes) {
191200
if (!userAssemblies.ContainsKey (td.Module.Assembly.Name.Name) || JavaTypeScanner.ShouldSkipJavaCallableWrapperGeneration (td, cache)) {
@@ -195,11 +204,27 @@ void Run (DirectoryAssemblyResolver res)
195204
javaTypes.Add (td);
196205
}
197206

198-
// Step 3 - Generate Java stub code
199-
var success = CreateJavaSources (javaTypes, cache);
207+
MarshalMethodsClassifier classifier = null;
208+
#if ENABLE_MARSHAL_METHODS
209+
if (!Debug) {
210+
classifier = new MarshalMethodsClassifier (cache, res, Log);
211+
}
212+
#endif
213+
// Step 2 - Generate Java stub code
214+
var success = CreateJavaSources (javaTypes, cache, classifier);
200215
if (!success)
201216
return;
202217

218+
#if ENABLE_MARSHAL_METHODS
219+
if (!Debug) {
220+
var rewriter = new MarshalMethodsAssemblyRewriter (classifier.MarshalMethods, classifier.Assemblies, marshalMethodsAssemblyPaths, Log);
221+
rewriter.Rewrite (res);
222+
}
223+
#endif
224+
// Step 3 - Generate type maps
225+
// Type mappings need to use all the assemblies, always.
226+
WriteTypeMappings (allJavaTypes, cache);
227+
203228
// We need to save a map of .NET type -> ACW type for resource file fixups
204229
var managed = new Dictionary<string, TypeDefinition> (javaTypes.Count, StringComparer.Ordinal);
205230
var java = new Dictionary<string, TypeDefinition> (javaTypes.Count, StringComparer.Ordinal);
@@ -210,7 +235,7 @@ void Run (DirectoryAssemblyResolver res)
210235
using (var acw_map = MemoryStreamPool.Shared.CreateStreamWriter ()) {
211236
foreach (TypeDefinition type in javaTypes) {
212237
string managedKey = type.FullName.Replace ('/', '.');
213-
string javaKey = JavaNativeTypeManager.ToJniName (type).Replace ('/', '.');
238+
string javaKey = JavaNativeTypeManager.ToJniName (type, cache).Replace ('/', '.');
214239

215240
acw_map.Write (type.GetPartialAssemblyQualifiedName (cache));
216241
acw_map.Write (';');
@@ -332,17 +357,30 @@ void Run (DirectoryAssemblyResolver res)
332357
string applicationTemplateFile = "ApplicationRegistration.java";
333358
SaveResource (applicationTemplateFile, applicationTemplateFile, real_app_dir,
334359
template => template.Replace ("// REGISTER_APPLICATION_AND_INSTRUMENTATION_CLASSES_HERE", regCallsWriter.ToString ()));
360+
361+
#if ENABLE_MARSHAL_METHODS
362+
void StoreMarshalAssemblyPath (string name, ITaskItem asm)
363+
{
364+
if (Debug) {
365+
return;
366+
}
367+
368+
if (!marshalMethodsAssemblyPaths.TryGetValue (name, out HashSet<string> assemblyPaths)) {
369+
assemblyPaths = new HashSet<string> ();
370+
marshalMethodsAssemblyPaths.Add (name, assemblyPaths);
371+
}
372+
373+
assemblyPaths.Add (asm.ItemSpec);
374+
}
375+
#endif
335376
}
336377

337-
bool CreateJavaSources (IEnumerable<TypeDefinition> javaTypes, TypeDefinitionCache cache)
378+
bool CreateJavaSources (IEnumerable<TypeDefinition> javaTypes, TypeDefinitionCache cache, MarshalMethodsClassifier classifier)
338379
{
339380
string outputPath = Path.Combine (OutputDirectory, "src");
340381
string monoInit = GetMonoInitSource (AndroidSdkPlatform);
341382
bool hasExportReference = ResolvedAssemblies.Any (assembly => Path.GetFileName (assembly.ItemSpec) == "Mono.Android.Export.dll");
342383
bool generateOnCreateOverrides = int.Parse (AndroidSdkPlatform) <= 10;
343-
#if ENABLE_MARSHAL_METHODS
344-
var overriddenMethodDescriptors = new List<OverriddenMethodDescriptor> ();
345-
#endif
346384

347385
bool ok = true;
348386
foreach (var t in javaTypes) {
@@ -353,15 +391,19 @@ bool CreateJavaSources (IEnumerable<TypeDefinition> javaTypes, TypeDefinitionCac
353391

354392
using (var writer = MemoryStreamPool.Shared.CreateStreamWriter ()) {
355393
try {
356-
var jti = new JavaCallableWrapperGenerator (t, Log.LogWarning, cache) {
394+
var jti = new JavaCallableWrapperGenerator (t, Log.LogWarning, cache, classifier) {
357395
GenerateOnCreateOverrides = generateOnCreateOverrides,
358396
ApplicationJavaClass = ApplicationJavaClass,
359397
MonoRuntimeInitialization = monoInit,
360398
};
361399

362400
jti.Generate (writer);
363401
#if ENABLE_MARSHAL_METHODS
364-
overriddenMethodDescriptors.AddRange (jti.OverriddenMethodDescriptors);
402+
if (!Debug) {
403+
if (classifier.FoundDynamicallyRegisteredMethods) {
404+
Log.LogWarning ($"Type '{t.GetAssemblyQualifiedName ()}' will register some of its Java override methods dynamically. This may adversely affect runtime performance. See preceding warnings for names of dynamically registered methods.");
405+
}
406+
}
365407
#endif
366408
writer.Flush ();
367409

@@ -397,7 +439,9 @@ bool CreateJavaSources (IEnumerable<TypeDefinition> javaTypes, TypeDefinitionCac
397439
}
398440
}
399441
#if ENABLE_MARSHAL_METHODS
400-
BuildEngine4.RegisterTaskObjectAssemblyLocal (MarshalMethodsRegisterTaskKey, overriddenMethodDescriptors, RegisteredTaskObjectLifetime.Build);
442+
if (!Debug) {
443+
BuildEngine4.RegisterTaskObjectAssemblyLocal (MarshalMethodsRegisterTaskKey, new MarshalMethodsState (classifier.MarshalMethods), RegisteredTaskObjectLifetime.Build);
444+
}
401445
#endif
402446
return ok;
403447
}

src/Xamarin.Android.Build.Tasks/Tasks/GeneratePackageManagerJava.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -442,10 +442,12 @@ void AddEnvironment ()
442442
};
443443
appConfigAsmGen.Init ();
444444
#if ENABLE_MARSHAL_METHODS
445-
var marshalMethodsAsmGen = new MarshalMethodsNativeAssemblyGenerator () {
445+
var marshalMethodsState = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal<MarshalMethodsState> (GenerateJavaStubs.MarshalMethodsRegisterTaskKey, RegisteredTaskObjectLifetime.Build);
446+
447+
var marshalMethodsAsmGen = new MarshalMethodsNativeAssemblyGenerator {
446448
NumberOfAssembliesInApk = assemblyCount,
447449
UniqueAssemblyNames = uniqueAssemblyNames,
448-
OverriddenMethodDescriptors = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal<List<Java.Interop.Tools.JavaCallableWrappers.OverriddenMethodDescriptor>> (GenerateJavaStubs.MarshalMethodsRegisterTaskKey, RegisteredTaskObjectLifetime.Build)
450+
MarshalMethods = marshalMethodsState?.MarshalMethods,
449451
};
450452
marshalMethodsAsmGen.Init ();
451453
#endif
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#if ENABLE_MARSHAL_METHODS
2+
using System;
3+
using System.Collections.Generic;
4+
using System.IO;
5+
6+
using Java.Interop.Tools.Cecil;
7+
using Microsoft.Android.Build.Tasks;
8+
using Microsoft.Build.Utilities;
9+
using Mono.Cecil;
10+
using Xamarin.Android.Tools;
11+
12+
namespace Xamarin.Android.Tasks
13+
{
14+
class MarshalMethodsAssemblyRewriter
15+
{
16+
IDictionary<string, IList<MarshalMethodEntry>> methods;
17+
ICollection<AssemblyDefinition> uniqueAssemblies;
18+
IDictionary <string, HashSet<string>> assemblyPaths;
19+
TaskLoggingHelper log;
20+
21+
public MarshalMethodsAssemblyRewriter (IDictionary<string, IList<MarshalMethodEntry>> methods, ICollection<AssemblyDefinition> uniqueAssemblies, IDictionary <string, HashSet<string>> assemblyPaths, TaskLoggingHelper log)
22+
{
23+
this.methods = methods ?? throw new ArgumentNullException (nameof (methods));
24+
this.uniqueAssemblies = uniqueAssemblies ?? throw new ArgumentNullException (nameof (uniqueAssemblies));
25+
this.assemblyPaths = assemblyPaths ?? throw new ArgumentNullException (nameof (assemblyPaths));
26+
this.log = log ?? throw new ArgumentNullException (nameof (log));
27+
}
28+
29+
public void Rewrite (DirectoryAssemblyResolver resolver)
30+
{
31+
MethodDefinition unmanagedCallersOnlyAttributeCtor = GetUnmanagedCallersOnlyAttributeConstructor (resolver);
32+
var unmanagedCallersOnlyAttributes = new Dictionary<AssemblyDefinition, CustomAttribute> ();
33+
foreach (AssemblyDefinition asm in uniqueAssemblies) {
34+
unmanagedCallersOnlyAttributes.Add (asm, CreateImportedUnmanagedCallersOnlyAttribute (asm, unmanagedCallersOnlyAttributeCtor));
35+
}
36+
37+
Console.WriteLine ("Adding the [UnmanagedCallersOnly] attribute to native callback methods and removing unneeded fields+methods");
38+
foreach (IList<MarshalMethodEntry> methodList in methods.Values) {
39+
foreach (MarshalMethodEntry method in methodList) {
40+
Console.WriteLine ($"\t{method.NativeCallback.FullName} (token: 0x{method.NativeCallback.MetadataToken.RID:x})");
41+
method.NativeCallback.CustomAttributes.Add (unmanagedCallersOnlyAttributes [method.NativeCallback.Module.Assembly]);
42+
method.Connector.DeclaringType.Methods.Remove (method.Connector);
43+
method.CallbackField?.DeclaringType.Fields.Remove (method.CallbackField);
44+
}
45+
}
46+
47+
Console.WriteLine ();
48+
Console.WriteLine ("Rewriting assemblies");
49+
50+
var newAssemblyPaths = new List<string> ();
51+
foreach (AssemblyDefinition asm in uniqueAssemblies) {
52+
foreach (string path in GetAssemblyPaths (asm)) {
53+
var writerParams = new WriterParameters {
54+
WriteSymbols = (File.Exists (path + ".mdb") || File.Exists (Path.ChangeExtension (path, ".pdb"))),
55+
};
56+
57+
string output = $"{path}.new";
58+
Console.WriteLine ($"\t{asm.Name} => {output}");
59+
asm.Write (output, writerParams);
60+
newAssemblyPaths.Add (output);
61+
}
62+
}
63+
64+
// Replace old versions of the assemblies only after we've finished rewriting without issues, otherwise leave the new
65+
// versions around.
66+
foreach (string path in newAssemblyPaths) {
67+
string target = Path.Combine (Path.GetDirectoryName (path), Path.GetFileNameWithoutExtension (path));
68+
MoveFile (path, target);
69+
70+
string source = Path.ChangeExtension (path, ".pdb");
71+
if (File.Exists (source)) {
72+
target = Path.ChangeExtension (Path.Combine (Path.GetDirectoryName (source), Path.GetFileNameWithoutExtension (source)), ".pdb");
73+
74+
MoveFile (source, target);
75+
}
76+
77+
source = $"{path}.mdb";
78+
if (File.Exists (source)) {
79+
target = Path.ChangeExtension (path, ".mdb");
80+
MoveFile (source, target);
81+
}
82+
}
83+
84+
Console.WriteLine ();
85+
Console.WriteLine ("Method tokens:");
86+
foreach (IList<MarshalMethodEntry> methodList in methods.Values) {
87+
foreach (MarshalMethodEntry method in methodList) {
88+
Console.WriteLine ($"\t{method.NativeCallback.FullName} (token: 0x{method.NativeCallback.MetadataToken.RID:x})");
89+
}
90+
}
91+
92+
void MoveFile (string source, string target)
93+
{
94+
Console.WriteLine ($"Moving '{source}' => '{target}'");
95+
Files.CopyIfChanged (source, target);
96+
try {
97+
File.Delete (source);
98+
} catch (Exception ex) {
99+
log.LogWarning ($"Unable to delete source file '{source}' when moving it to '{target}'");
100+
}
101+
}
102+
}
103+
104+
ICollection<string> GetAssemblyPaths (AssemblyDefinition asm)
105+
{
106+
if (!assemblyPaths.TryGetValue (asm.Name.Name, out HashSet<string> paths)) {
107+
throw new InvalidOperationException ($"Unable to determine file path for assembly '{asm.Name.Name}'");
108+
}
109+
110+
return paths;
111+
}
112+
113+
MethodDefinition GetUnmanagedCallersOnlyAttributeConstructor (DirectoryAssemblyResolver resolver)
114+
{
115+
AssemblyDefinition asm = resolver.Resolve ("System.Runtime.InteropServices");
116+
TypeDefinition unmanagedCallersOnlyAttribute = null;
117+
foreach (ModuleDefinition md in asm.Modules) {
118+
foreach (ExportedType et in md.ExportedTypes) {
119+
if (!et.IsForwarder) {
120+
continue;
121+
}
122+
123+
if (String.Compare ("System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute", et.FullName, StringComparison.Ordinal) != 0) {
124+
continue;
125+
}
126+
127+
unmanagedCallersOnlyAttribute = et.Resolve ();
128+
break;
129+
}
130+
}
131+
132+
if (unmanagedCallersOnlyAttribute == null) {
133+
throw new InvalidOperationException ("Unable to find the System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute type");
134+
}
135+
136+
foreach (MethodDefinition md in unmanagedCallersOnlyAttribute.Methods) {
137+
if (!md.IsConstructor) {
138+
continue;
139+
}
140+
141+
return md;
142+
}
143+
144+
throw new InvalidOperationException ("Unable to find the System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute type constructor");
145+
}
146+
147+
CustomAttribute CreateImportedUnmanagedCallersOnlyAttribute (AssemblyDefinition targetAssembly, MethodDefinition unmanagedCallersOnlyAtributeCtor)
148+
{
149+
return new CustomAttribute (targetAssembly.MainModule.ImportReference (unmanagedCallersOnlyAtributeCtor));
150+
}
151+
}
152+
}
153+
#endif

0 commit comments

Comments
 (0)