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
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -667,11 +667,16 @@ static void SkipArrayShape (ref BlobReader reader)
var methods = new List<MarshalMethodInfo> ();
var fields = new List<JavaFieldInfo> ();
HashSet<string>? 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);

Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -2611,9 +2649,8 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List<J
{
foreach (var caHandle in methodDef.GetCustomAttributes ()) {
var ca = index.Reader.GetCustomAttribute (caHandle);
var attrName = index.GetCustomAttributeName (ca);

if (attrName != "ExportFieldAttribute") {
if (!IsExportFieldAttribute (ca, index)) {
continue;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN
log.LogCodedError ("XA4251", Properties.Resources.XA4251, managedTypeName);
public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) =>
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) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Comment thread
simonrozsival marked this conversation as resolved.
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 ? "<T>" : "";
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 ()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}

// ---------------------------------------------------------------
Expand Down Expand Up @@ -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]
Expand Down
Loading