Skip to content

Commit 6a27915

Browse files
authored
[XABT] Separate marshal method storage from MarshalMethodClassifier to MarshalMethodCollection. (#10077)
Today the marshal method scanning process is tightly coupled to scanning for JavaCallableWrappers. That is, when building `CallableWrapperType` objects, the `MarshalMethodsClassifier` is used to classify methods as marshal methods by calling `ShouldBeDynamicallyRegistered (...)`. However `ShouldBeDynamicallyRegistered` doesn't just return a simple `bool`, it has the side effect of building a list of `MarshalMethodEntry` objects that are used extensively throughout the build process. We are likely going to need to build this list in places other than JavaCallableWrappers scanning: assembly rewriting will likely be before JCWs, and `GenerateNativeMarshalMethodSources` will likely be after JCWs. As such: - Rework `MarshalMethodsClassifier` to simply classify methods as LLVM marshal method eligible or not (dynamically registered). - Move storage to a new `MarshalMethodsCollection`. Additionally, rework `MarshalMethodsClassifier` to also be able to detect marshal methods that have already been rewritten and provide the information later steps (`GenerateJavaStubs`, `GenerateNativeMarshalMethodSources`) need to function properly. This isn't in use yet, but will be needed in the future.
1 parent cc28c89 commit 6a27915

File tree

13 files changed

+863
-313
lines changed

13 files changed

+863
-313
lines changed

src/Xamarin.Android.Build.Tasks/Linker/MonoDroid.Tuner/FindJavaObjectsStep.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,10 @@ List<CallableWrapperType> ConvertToCallableWrappers (List<TypeDefinition> types)
114114
DefaultMonoRuntimeInitialization = "mono.MonoPackageManager.LoadApplication (context);",
115115
};
116116

117-
if (UseMarshalMethods)
118-
reader_options.MethodClassifier = new MarshalMethodsClassifier (Context, Context.Resolver, Log);
117+
if (UseMarshalMethods) {
118+
var classifier = new MarshalMethodsClassifier (Context, Context.Resolver, Log);
119+
reader_options.MethodClassifier = new MarshalMethodsCollection (classifier);
120+
}
119121

120122
foreach (var type in types) {
121123
var wrapper = CecilImporter.CreateType (type, Context, reader_options);

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -214,25 +214,28 @@ internal static Dictionary<string, ITaskItem> MaybeGetArchAssemblies (Dictionary
214214
XAAssemblyResolver resolver = MakeResolver (useMarshalMethods, arch, assemblies);
215215
var tdCache = new TypeDefinitionCache ();
216216
(List<TypeDefinition> allJavaTypes, List<TypeDefinition> javaTypesForJCW) = ScanForJavaTypes (resolver, tdCache, assemblies, userAssemblies, useMarshalMethods);
217-
var jcwContext = new JCWGeneratorContext (arch, resolver, assemblies.Values, javaTypesForJCW, tdCache, useMarshalMethods);
217+
var jcwContext = new JCWGeneratorContext (arch, resolver, assemblies.Values, javaTypesForJCW, tdCache);
218218
var jcwGenerator = new JCWGenerator (Log, jcwContext) {
219219
CodeGenerationTarget = codeGenerationTarget,
220220
};
221-
bool success;
221+
bool success = true;
222222

223223
if (generateJavaCode && RunCheckedBuild) {
224-
success = jcwGenerator.GenerateAndClassify (AndroidSdkPlatform, outputPath: Path.Combine (OutputDirectory, "src"), ApplicationJavaClass);
224+
success = jcwGenerator.Generate (AndroidSdkPlatform, outputPath: Path.Combine (OutputDirectory, "src"), ApplicationJavaClass);
225225

226226
generatedJavaFiles = jcwGenerator.GeneratedJavaFiles;
227-
} else {
228-
success = jcwGenerator.Classify (AndroidSdkPlatform);
229227
}
230228

231229
if (!success) {
232230
return (false, null);
233231
}
234232

235-
return (true, new NativeCodeGenState (arch, tdCache, resolver, allJavaTypes, javaTypesForJCW, jcwGenerator.Classifier));
233+
MarshalMethodsCollection? marshalMethodsCollection = null;
234+
235+
if (useMarshalMethods)
236+
marshalMethodsCollection = MarshalMethodsCollection.FromAssemblies (arch, assemblies.Values.ToList (), resolver, Log);
237+
238+
return (true, new NativeCodeGenState (arch, tdCache, resolver, allJavaTypes, javaTypesForJCW, marshalMethodsCollection));
236239
}
237240

238241
(List<TypeDefinition> allJavaTypes, List<TypeDefinition> javaTypesForJCW) ScanForJavaTypes (XAAssemblyResolver res, TypeDefinitionCache cache, Dictionary<string, ITaskItem> assemblies, Dictionary<string, ITaskItem> userAssemblies, bool useMarshalMethods)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ void GenerateAdditionalProviderSources (NativeCodeGenState codeGenState, IList<s
179179
regCallsWriter.WriteLine ("// Application and Instrumentation ACWs must be registered first.");
180180
foreach (TypeDefinition type in codeGenState.JavaTypesForJCW) {
181181
if (JavaNativeTypeManager.IsApplication (type, codeGenState.TypeCache) || JavaNativeTypeManager.IsInstrumentation (type, codeGenState.TypeCache)) {
182-
if (codeGenState.Classifier != null && !codeGenState.Classifier.FoundDynamicallyRegisteredMethods (type)) {
182+
if (codeGenState.Classifier != null && !codeGenState.Classifier.TypeHasDynamicallyRegisteredMethods (type)) {
183183
continue;
184184
}
185185

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Collections.Concurrent;
2+
using System.Linq;
23
using Microsoft.Android.Build.Tasks;
34
using Microsoft.Build.Framework;
45
using Xamarin.Android.Tools;
@@ -50,13 +51,15 @@ public override bool RunTask ()
5051
}
5152

5253
Log.LogDebugMessage ($"[{state.TargetArch}] Number of generated marshal methods: {state.Classifier.MarshalMethods.Count}");
53-
if (state.Classifier.RejectedMethodCount > 0) {
54-
Log.LogWarning ($"[{state.TargetArch}] Number of methods in the project that will be registered dynamically: {state.Classifier.RejectedMethodCount}");
54+
if (state.Classifier.DynamicallyRegisteredMarshalMethods.Count > 0) {
55+
Log.LogWarning ($"[{state.TargetArch}] Number of methods in the project that will be registered dynamically: {state.Classifier.DynamicallyRegisteredMarshalMethods.Count}");
5556
}
5657

57-
if (state.Classifier.WrappedMethodCount > 0) {
58+
var wrappedCount = state.Classifier.MarshalMethods.Sum (m => m.Value.Count (m2 => m2.NeedsBlittableWorkaround));
59+
60+
if (wrappedCount > 0) {
5861
// TODO: change to LogWarning once the generator can output code which requires no non-blittable wrappers
59-
Log.LogDebugMessage ($"[{state.TargetArch}] Number of methods in the project that need marshal method wrappers: {state.Classifier.WrappedMethodCount}");
62+
Log.LogDebugMessage ($"[{state.TargetArch}] Number of methods in the project that need marshal method wrappers: {wrappedCount}");
6063
}
6164
}
6265

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#nullable enable
2+
using System;
3+
using System.IO;
4+
using System.Text;
5+
using Microsoft.Build.Framework;
6+
using Microsoft.Build.Utilities;
7+
using NUnit.Framework;
8+
using Xamarin.Android.Tasks;
9+
using Xamarin.ProjectTools;
10+
11+
namespace Xamarin.Android.Build.Tests;
12+
13+
public class MarshalMethodTests : BaseTest
14+
{
15+
[Test]
16+
public void MarshalMethodsCollectionScanning ()
17+
{
18+
// This test does 2 things:
19+
// - Builds a binding project in Debug mode to create an assembly that contains convertible
20+
// marshal methods to ensure they are found by MarshalMethodsCollection
21+
// - Builds the same project in Release mode which rewrites the assembly to ensure those
22+
// same marshal methods can be found after they are rewritten
23+
var proj = new XamarinAndroidApplicationProject {
24+
ProjectName = "mmtest",
25+
};
26+
27+
proj.Sources.Add (new AndroidItem.AndroidLibrary ("javaclasses.jar") {
28+
BinaryContent = () => ResourceData.JavaSourceJarTestJar,
29+
});
30+
31+
proj.AndroidJavaSources.Add (new AndroidItem.AndroidJavaSource ("JavaSourceTestInterface.java") {
32+
Encoding = Encoding.ASCII,
33+
TextContent = () => ResourceData.JavaSourceTestInterface,
34+
Metadata = { { "Bind", "True" } },
35+
});
36+
37+
proj.AndroidJavaSources.Add (new AndroidItem.AndroidJavaSource ("JavaSourceTestExtension.java") {
38+
Encoding = Encoding.ASCII,
39+
TextContent = () => ResourceData.JavaSourceTestExtension,
40+
Metadata = { { "Bind", "True" } },
41+
});
42+
43+
proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_MAINACTIVITY}", """
44+
// Implements Java interface method
45+
class MyGreeter : Java.Lang.Object, Com.Xamarin.Android.Test.Msbuildtest.IJavaSourceTestInterface {
46+
public virtual string? GreetWithQuestion (string? p0, Java.Util.Date? p1, string? p2) => "greetings!";
47+
}
48+
49+
// Overrides implemented Java interface method
50+
class MyExtendedGreeter : MyGreeter {
51+
public override string? GreetWithQuestion (string? p0, Java.Util.Date? p1, string? p2) => "more greetings!";
52+
}
53+
54+
// Implements Java interface method (duplicate)
55+
class MyGreeter2 : Java.Lang.Object, Com.Xamarin.Android.Test.Msbuildtest.IJavaSourceTestInterface {
56+
public virtual string? GreetWithQuestion (string? p0, Java.Util.Date? p1, string? p2) => "duplicate greetings!";
57+
}
58+
59+
// Overrides Java class method
60+
class MyOverriddenGreeter : Com.Xamarin.Android.Test.Msbuildtest.JavaSourceTestExtension {
61+
public override string? GreetWithQuestion (string? p0, Java.Util.Date? p1, string? p2) => "even more greetings!";
62+
}
63+
""");
64+
65+
var builder = CreateApkBuilder ();
66+
Assert.IsTrue (builder.Build (proj), "`dotnet build` should succeed");
67+
builder.AssertHasNoWarnings ();
68+
69+
var intermediateDebugOutputPath = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath, "android", "assets", "arm64-v8a");
70+
var outputDebugDll = Path.Combine (intermediateDebugOutputPath, $"{proj.ProjectName}.dll");
71+
72+
var log = new TaskLoggingHelper (new MockBuildEngine (TestContext.Out, [], [], []), nameof (MarshalMethodsCollectionScanning));
73+
var xaResolver = new XAAssemblyResolver (Tools.AndroidTargetArch.Arm64, log, false);
74+
xaResolver.SearchDirectories.Add (Path.GetDirectoryName (outputDebugDll)!);
75+
76+
var collection = MarshalMethodsCollection.FromAssemblies (Tools.AndroidTargetArch.Arm64, [CreateItem (outputDebugDll, "arm64-v8a")], xaResolver, log);
77+
78+
Assert.AreEqual (3, collection.MarshalMethods.Count);
79+
Assert.AreEqual (0, collection.ConvertedMarshalMethods.Count);
80+
81+
var key1 = "Android.App.Activity, Mono.Android\tOnCreate";
82+
var key2 = "Com.Xamarin.Android.Test.Msbuildtest.IJavaSourceTestInterface, mmtest\tGreetWithQuestion";
83+
var key3 = "Com.Xamarin.Android.Test.Msbuildtest.JavaSourceTestExtension, mmtest\tGreetWithQuestion";
84+
85+
Assert.AreEqual (1, collection.MarshalMethods [key1].Count);
86+
Assert.AreEqual (2, collection.MarshalMethods [key2].Count);
87+
Assert.AreEqual (1, collection.MarshalMethods [key3].Count);
88+
89+
AssertMarshalMethodData (collection.MarshalMethods [key1] [0],
90+
callbackField: null,
91+
connector: "System.Delegate Android.App.Activity::GetOnCreate_Landroid_os_Bundle_Handler()",
92+
declaringType: "mmtest.MainActivity",
93+
implementedMethod: "System.Void mmtest.MainActivity::OnCreate(Android.OS.Bundle)",
94+
jniMethodName: "onCreate",
95+
jniMethodSignature: "(Landroid/os/Bundle;)V",
96+
jniTypeName: "com/xamarin/marshalmethodscollectionscanning/MainActivity",
97+
nativeCallback: "System.Void Android.App.Activity::n_OnCreate_Landroid_os_Bundle_(System.IntPtr,System.IntPtr,System.IntPtr)",
98+
registeredMethod: "System.Void Android.App.Activity::OnCreate(Android.OS.Bundle)");
99+
100+
AssertMarshalMethodData (collection.MarshalMethods [key2] [0],
101+
callbackField: null,
102+
connector: "System.Delegate Com.Xamarin.Android.Test.Msbuildtest.IJavaSourceTestInterfaceInvoker::GetGreetWithQuestion_Ljava_lang_String_Ljava_util_Date_Ljava_lang_String_Handler()",
103+
declaringType: "mmtest.MyGreeter",
104+
implementedMethod: "System.String Com.Xamarin.Android.Test.Msbuildtest.IJavaSourceTestInterface::GreetWithQuestion(System.String,Java.Util.Date,System.String)",
105+
jniMethodName: "greetWithQuestion",
106+
jniMethodSignature: "(Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;",
107+
jniTypeName: "crc644a923d2fc5ca7023/MyGreeter",
108+
nativeCallback: "System.IntPtr Com.Xamarin.Android.Test.Msbuildtest.IJavaSourceTestInterfaceInvoker::n_GreetWithQuestion_Ljava_lang_String_Ljava_util_Date_Ljava_lang_String_(System.IntPtr,System.IntPtr,System.IntPtr,System.IntPtr,System.IntPtr)",
109+
registeredMethod: "System.String Com.Xamarin.Android.Test.Msbuildtest.IJavaSourceTestInterface::GreetWithQuestion(System.String,Java.Util.Date,System.String)");
110+
111+
AssertMarshalMethodData (collection.MarshalMethods [key2] [1],
112+
callbackField: null,
113+
connector: "System.Delegate Com.Xamarin.Android.Test.Msbuildtest.IJavaSourceTestInterfaceInvoker::GetGreetWithQuestion_Ljava_lang_String_Ljava_util_Date_Ljava_lang_String_Handler()",
114+
declaringType: "mmtest.MyGreeter2",
115+
implementedMethod: "System.String Com.Xamarin.Android.Test.Msbuildtest.IJavaSourceTestInterface::GreetWithQuestion(System.String,Java.Util.Date,System.String)",
116+
jniMethodName: "greetWithQuestion",
117+
jniMethodSignature: "(Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;",
118+
jniTypeName: "crc644a923d2fc5ca7023/MyGreeter2",
119+
nativeCallback: "System.IntPtr Com.Xamarin.Android.Test.Msbuildtest.IJavaSourceTestInterfaceInvoker::n_GreetWithQuestion_Ljava_lang_String_Ljava_util_Date_Ljava_lang_String_(System.IntPtr,System.IntPtr,System.IntPtr,System.IntPtr,System.IntPtr)",
120+
registeredMethod: "System.String Com.Xamarin.Android.Test.Msbuildtest.IJavaSourceTestInterface::GreetWithQuestion(System.String,Java.Util.Date,System.String)");
121+
122+
AssertMarshalMethodData (collection.MarshalMethods [key3] [0],
123+
callbackField: null,
124+
connector: "System.Delegate Com.Xamarin.Android.Test.Msbuildtest.JavaSourceTestExtension::GetGreetWithQuestion_Ljava_lang_String_Ljava_util_Date_Ljava_lang_String_Handler()",
125+
declaringType: "mmtest.MyOverriddenGreeter",
126+
implementedMethod: "System.String mmtest.MyOverriddenGreeter::GreetWithQuestion(System.String,Java.Util.Date,System.String)",
127+
jniMethodName: "greetWithQuestion",
128+
jniMethodSignature: "(Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;)Ljava/lang/String;",
129+
jniTypeName: "crc644a923d2fc5ca7023/MyOverriddenGreeter",
130+
nativeCallback: "System.IntPtr Com.Xamarin.Android.Test.Msbuildtest.JavaSourceTestExtension::n_GreetWithQuestion_Ljava_lang_String_Ljava_util_Date_Ljava_lang_String_(System.IntPtr,System.IntPtr,System.IntPtr,System.IntPtr,System.IntPtr)",
131+
registeredMethod: "System.String Com.Xamarin.Android.Test.Msbuildtest.JavaSourceTestExtension::GreetWithQuestion(System.String,Java.Util.Date,System.String)");
132+
133+
// Recompile with Release so marshal methods get rewritten
134+
proj.IsRelease = true;
135+
136+
Assert.IsTrue (builder.Build (proj), "`dotnet build` should succeed");
137+
builder.AssertHasNoWarnings ();
138+
139+
// Rescan for modified marshal methods
140+
var intermediateReleaseOutputPath = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath, "android-arm64", "linked");
141+
var outputReleaseDll = Path.Combine (intermediateReleaseOutputPath, $"{proj.ProjectName}.dll");
142+
143+
xaResolver = new XAAssemblyResolver (Tools.AndroidTargetArch.Arm64, log, false);
144+
xaResolver.SearchDirectories.Add (Path.GetDirectoryName (outputReleaseDll)!);
145+
146+
var releaseCollection = MarshalMethodsCollection.FromAssemblies (Tools.AndroidTargetArch.Arm64, [CreateItem (outputReleaseDll, "arm64-v8a")], xaResolver, log);
147+
148+
Assert.AreEqual (0, releaseCollection.MarshalMethods.Count);
149+
Assert.AreEqual (3, releaseCollection.ConvertedMarshalMethods.Count);
150+
151+
AssertRewrittenMethodData (releaseCollection.ConvertedMarshalMethods [key1] [0], collection.MarshalMethods [key1] [0]);
152+
AssertRewrittenMethodData (releaseCollection.ConvertedMarshalMethods [key2] [0], collection.MarshalMethods [key2] [0]);
153+
AssertRewrittenMethodData (releaseCollection.ConvertedMarshalMethods [key2] [1], collection.MarshalMethods [key2] [1]);
154+
AssertRewrittenMethodData (releaseCollection.ConvertedMarshalMethods [key3] [0], collection.MarshalMethods [key3] [0]);
155+
}
156+
157+
void AssertMarshalMethodData (MarshalMethodEntry entry, string? callbackField, string? connector, string? declaringType,
158+
string? implementedMethod, string? jniMethodName, string? jniMethodSignature, string? jniTypeName,
159+
string? nativeCallback, string? registeredMethod)
160+
{
161+
Assert.AreEqual (callbackField, entry.CallbackField?.ToString (), "Callback field should be the same.");
162+
Assert.AreEqual (connector, entry.Connector?.ToString (), "Connector should be the same.");
163+
Assert.AreEqual (declaringType, entry.DeclaringType.ToString (), "Declaring type should be the same.");
164+
Assert.AreEqual (implementedMethod, entry.ImplementedMethod?.ToString (), "Implemented method should be the same.");
165+
Assert.AreEqual (jniMethodName, entry.JniMethodName, "JNI method name should be the same.");
166+
Assert.AreEqual (jniMethodSignature, entry.JniMethodSignature, "JNI method signature should be the same.");
167+
Assert.AreEqual (jniTypeName, entry.JniTypeName, "JNI type name should be the same.");
168+
Assert.AreEqual (nativeCallback, entry.NativeCallback.ToString (), "Native callback should be the same.");
169+
Assert.AreEqual (registeredMethod, entry.RegisteredMethod?.ToString (), "Registered method should be the same.");
170+
}
171+
172+
void AssertRewrittenMethodData (ConvertedMarshalMethodEntry converted, MarshalMethodEntry entry)
173+
{
174+
// Things that are different between the two:
175+
Assert.IsNull (converted.CallbackField, "Callback field will be null.");
176+
Assert.IsNull (converted.Connector, "Connector will be null.");
177+
178+
var nativeCallback = converted.NativeCallback?.ToString () ?? "";
179+
var paren = nativeCallback.IndexOf ('(');
180+
var convertedNativeCallback = nativeCallback.Substring (0, paren) + "_mm_wrapper" + nativeCallback.Substring (paren);
181+
Assert.AreEqual (convertedNativeCallback, converted.ConvertedNativeCallback?.ToString (), "ConvertedNativeCallback should be the same.");
182+
183+
// Things that should be the same between the two:
184+
Assert.AreEqual (entry.DeclaringType.ToString (), converted.DeclaringType.ToString (), "Declaring type should be the same.");
185+
Assert.AreEqual (entry.ImplementedMethod?.ToString (), converted.ImplementedMethod?.ToString (), "Implemented method should be the same.");
186+
Assert.AreEqual (entry.JniMethodName, converted.JniMethodName, "JNI method name should be the same.");
187+
Assert.AreEqual (entry.JniMethodSignature, converted.JniMethodSignature, "JNI method signature should be the same.");
188+
Assert.AreEqual (entry.JniTypeName, converted.JniTypeName, "JNI type name should be the same.");
189+
Assert.AreEqual (entry.NativeCallback.ToString (), converted.NativeCallback?.ToString (), "Native callback should be the same.");
190+
Assert.AreEqual (entry.RegisteredMethod?.ToString (), converted.RegisteredMethod?.ToString (), "Registered method should be the same.");
191+
}
192+
193+
static ITaskItem CreateItem (string itemSpec, string abi)
194+
{
195+
var item = new TaskItem (itemSpec);
196+
item.SetMetadata ("Abi", abi);
197+
return item;
198+
}
199+
}

0 commit comments

Comments
 (0)