diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index ae25273fa8a..879bf42aafc 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -22,6 +22,9 @@ void LogUnresolvableJavaPeerSkippedWarning ( string unresolvedAssemblyPath); void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName); void LogInvalidJavaNameError (string javaName, string invalidIdentifier); + void LogExportFieldWithParametersError (); + void LogExportFieldOnGenericTypeError (); + void LogExportFieldReturnsVoidError (); void LogCustomJavaObjectError (string managedTypeName); void LogCustomJavaObjectWarning (string managedTypeName); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 989afeda295..164d4c9170d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -667,11 +667,16 @@ static void SkipArrayShape (ref BlobReader reader) var methods = new List (); var fields = new List (); HashSet? registeredMethodKeys = detectBaseOverrides ? new (StringComparer.Ordinal) : null; + bool isGenericType = typeDef.GetGenericParameters ().Count > 0; // Pass 1: collect methods with [Register], [Export], or [ExportField] directly on them foreach (var methodHandle in typeDef.GetMethods ()) { var methodDef = index.Reader.GetMethodDefinition (methodHandle); + if (!ValidateExportField (methodDef, index, isGenericType)) { + continue; + } + // Check for [ExportField] — produces both a marshal method AND a field CollectExportField (methodDef, index, fields); @@ -735,6 +740,39 @@ static void SkipArrayShape (ref BlobReader reader) return (methods, fields); } + static bool IsExportFieldAttribute (CustomAttribute attribute, AssemblyIndex index) + { + return AssemblyIndex.IsCustomAttributeMatch (attribute, index.Reader, "Java.Interop", "ExportFieldAttribute"); + } + + bool ValidateExportField (MethodDefinition methodDef, AssemblyIndex index, bool isGenericType) + { + foreach (var caHandle in methodDef.GetCustomAttributes ()) { + var ca = index.Reader.GetCustomAttribute (caHandle); + if (!IsExportFieldAttribute (ca, index)) { + continue; + } + + if (isGenericType) { + logger?.LogExportFieldOnGenericTypeError (); + return false; + } + + var sig = methodDef.DecodeSignature (index.TypeRefSignatureProvider, index); + if (sig.ParameterTypes.Length != 0) { + logger?.LogExportFieldWithParametersError (); + return false; + } + if (sig.ReturnType.ManagedTypeName == "System.Void") { + logger?.LogExportFieldReturnsVoidError (); + return false; + } + return true; + } + + return true; + } + static bool HasJniAddNativeMethodRegistrationAttribute (TypeDefinition typeDef, AssemblyIndex index) { const string JniAddNativeMethodRegistrationAttribute = "JniAddNativeMethodRegistrationAttribute"; @@ -1707,7 +1745,7 @@ bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, return true; } - if (attrName == "ExportFieldAttribute") { + if (IsExportFieldAttribute (ca, index)) { (registerInfo, exportInfo) = ParseExportFieldAsMethod (ca, methodDef, index); return true; } @@ -2611,9 +2649,8 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List log.LogCodedError ("XA4258", Properties.Resources.XA4258, javaName, invalidIdentifier); + public void LogExportFieldWithParametersError () => + log.LogCodedError ("XA4205", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4205); + public void LogExportFieldOnGenericTypeError () => + log.LogCodedError ("XA4207", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4207); + public void LogExportFieldReturnsVoidError () => + log.LogCodedError ("XA4208", Java.Interop.Localization.Resources.JavaCallableWrappers_XA4208); public void LogCustomJavaObjectError (string managedTypeName) => log.LogError ("{0}", $"XA4212: {string.Format (Properties.Resources.XA4212, managedTypeName)}"); public void LogCustomJavaObjectWarning (string managedTypeName) => diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index b5512e330fe..95cfe4443d3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -36,6 +36,98 @@ public void Build_WithTrimmableTypeMap_Succeeds ([Values] bool isRelease, [Value AssertTrimmableTypeMapOutputs (intermediateDir); } + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "parameters", "XA4205")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "parameters", "XA4205")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "parameters", "XA4205")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "void", "XA4208")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "void", "XA4208")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "void", "XA4208")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic", "XA4207")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic", "XA4207")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic", "XA4207")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "parameters-and-void", "XA4205")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "parameters-and-void", "XA4205")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "parameters-and-void", "XA4205")] + [TestCase ("llvm-ir", AndroidRuntime.CoreCLR, "generic-parameters-and-void", "XA4207")] + [TestCase ("trimmable", AndroidRuntime.CoreCLR, "generic-parameters-and-void", "XA4207")] + [TestCase ("trimmable", AndroidRuntime.NativeAOT, "generic-parameters-and-void", "XA4207")] + public void Build_InvalidExportField_ReportsLegacyDiagnostic ( + string typeMapImplementation, + AndroidRuntime runtime, + string invalidShape, + string expectedCode) + { + bool isRelease = runtime == AndroidRuntime.NativeAOT; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + var initializer = invalidShape switch { + "parameters" => "public int InitialValue (int value) => value;", + "void" => "public void InitialValue () { }", + "generic" => "public int InitialValue () => 42;", + "parameters-and-void" => "public void InitialValue (int value) { }", + "generic-parameters-and-void" => "public void InitialValue (int value) { }", + _ => throw new InvalidOperationException ($"Unknown invalid [ExportField] shape '{invalidShape}'."), + }; + var proj = CreateExportFieldValidationProject (runtime, typeMapImplementation, $""" + [ExportField ("VALUE")] + {initializer} + """, genericType: invalidShape.StartsWith ("generic", StringComparison.Ordinal)); + + using var builder = CreateApkBuilder (); + builder.ThrowOnBuildFailure = false; + Assert.IsFalse (builder.Build (proj), $"{runtime}/{typeMapImplementation} should reject {invalidShape} [ExportField] initializers."); + StringAssertEx.Contains ($"error {expectedCode}", builder.LastBuildOutput, $"The build should report {expectedCode}."); + if (invalidShape == "parameters-and-void") { + Assert.IsFalse ( + builder.LastBuildOutput.Any (line => line.Contains ("error XA4208", StringComparison.Ordinal)), + "XA4205 should take precedence over XA4208, matching LLVM-IR." + ); + } else if (invalidShape == "generic-parameters-and-void") { + Assert.IsFalse ( + builder.LastBuildOutput.Any (line => + line.Contains ("error XA4205", StringComparison.Ordinal) || + line.Contains ("error XA4208", StringComparison.Ordinal)), + "XA4207 should take precedence over initializer signature diagnostics, matching LLVM-IR." + ); + } + } + + static XamarinAndroidApplicationProject CreateExportFieldValidationProject ( + AndroidRuntime runtime, + string typeMapImplementation, + string members, + bool genericType = false) + { + var typeParameters = genericType ? "" : ""; + var proj = new XamarinAndroidApplicationProject { + IsRelease = runtime == AndroidRuntime.NativeAOT, + References = { + new BuildItem.Reference ("Mono.Android.Export"), + }, + }; + proj.SetRuntime (runtime); + proj.SetProperty ("AndroidTypeMapImplementation", typeMapImplementation); + proj.Sources.Add (new BuildItem.Source ("ExportFieldValidation.cs") { + TextContent = () => $$""" + using Android.Runtime; + using Java.Interop; + + namespace ExportFieldValidation { + [Register ("com/example/exportfields/ValidationPeer")] + class ValidationPeer{{typeParameters}} : Java.Lang.Object { + public ValidationPeer () { + } + + {{members}} + } + } + """, + }); + return proj; + } + [Test] public void Build_PublishAotProject_UsesTrimmableTypeMapForCoreClrDebug () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 89075a5832d..b18904a7ec9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -53,6 +53,12 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN logMessages.Add ($"XA4251: Type '{managedTypeName}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map."); public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) => logMessages.Add ($"XA4258: Java name '{javaName}' contains reserved Java identifier '{invalidIdentifier}'."); + public void LogExportFieldWithParametersError () => + logMessages.Add ("XA4205: [ExportField] can only be used on methods with 0 parameters."); + public void LogExportFieldReturnsVoidError () => + logMessages.Add ("XA4208: [ExportField] cannot be used on a method returning 'void'."); + public void LogExportFieldOnGenericTypeError () => + logMessages.Add ("XA4207: [ExportField] cannot be used on a generic type."); public void LogCustomJavaObjectError (string managedTypeName) => logMessages.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported."); public void LogCustomJavaObjectWarning (string managedTypeName) => diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs index 188a14cc191..39461a8fba2 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs @@ -305,6 +305,16 @@ public void ExportField_StaticObject_InitializerVisibleFromJava () Assert.AreEqual (43, value.IntValue ()); } } + + [Test, Category ("Export")] + public void ExportField_InstancePrimitive_InitializerVisibleFromJava () + { + using var value = new ExportInstanceField (); + using var klass = Java.Lang.Class.FromType (typeof (ExportInstanceField)); + var field = JNIEnv.GetFieldID (klass.Handle, "ANSWER", "I"); + Assert.AreNotEqual (IntPtr.Zero, field, "JNI field id for ANSWER not found"); + Assert.AreEqual (42, JNIEnv.GetIntField (value.Handle, field)); + } } // --------------------------------------------------------------- @@ -341,6 +351,12 @@ class ExportStaticField : Java.Lang.Object public static Java.Lang.Integer GetObjectAnswer () => new Java.Lang.Integer (43); } + class ExportInstanceField : Java.Lang.Object + { + [ExportField ("ANSWER")] + public int GetAnswer () => 42; + } + class ExportString : Java.Lang.Object { [Export]