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 @@ -21,4 +21,6 @@ void LogUnresolvableJavaPeerSkippedWarning (
string unresolvedAssemblyName,
string unresolvedAssemblyPath);
void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName);
void LogCustomJavaObjectError (string managedTypeName);
void LogCustomJavaObjectWarning (string managedTypeName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ enum HashedPackageNamingPolicy {
readonly ITrimmableTypeMapLogger? logger;
readonly HashedPackageNamingPolicy packageNamingPolicy;
readonly HashSet<string> frameworkAssemblyNames;
readonly bool errorOnCustomJavaObject;

public JavaPeerScanner (string? packageNamingPolicy = null, ITrimmableTypeMapLogger? logger = null, HashSet<string>? frameworkAssemblyNames = null)
public JavaPeerScanner (string? packageNamingPolicy = null, ITrimmableTypeMapLogger? logger = null, HashSet<string>? frameworkAssemblyNames = null, bool errorOnCustomJavaObject = true)
{
this.packageNamingPolicy = ParsePackageNamingPolicy (packageNamingPolicy);
this.logger = logger;
this.frameworkAssemblyNames = frameworkAssemblyNames ?? new HashSet<string> (StringComparer.OrdinalIgnoreCase);
this.errorOnCustomJavaObject = errorOnCustomJavaObject;
}

/// <summary>
Expand Down Expand Up @@ -314,6 +316,17 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
if (ExtendsJavaPeer (typeDef, index)) {
(jniName, compatJniName) = ComputeAutoJniNames (typeDef, index);
} else {
// A managed class that implements Android.Runtime.IJavaObject but does not
// derive from a Java peer (Java.Lang.Object / Java.Lang.Throwable) cannot be
// marshaled to Java. Mirror the legacy XAJavaTypeScanner XA4212 diagnostic,
// which the managed/llvm-ir typemap paths raise via GenerateJavaStubs.
if (IsCustomJavaObject (typeDef, index)) {
if (errorOnCustomJavaObject) {
logger?.LogCustomJavaObjectError (fullName);
} else {
logger?.LogCustomJavaObjectWarning (fullName);
}
}
continue;
}
}
Expand Down Expand Up @@ -2126,6 +2139,103 @@ public void Dispose ()

readonly Dictionary<string, bool> extendsJavaPeerCache = new (StringComparer.Ordinal);

const string IJavaObjectFullName = "Android.Runtime.IJavaObject";

readonly Dictionary<string, bool> implementsIJavaObjectCache = new (StringComparer.Ordinal);

/// <summary>
/// Determines whether a type is a "custom" Java object: a managed class that implements
/// Android.Runtime.IJavaObject but does not derive from a Java peer (Java.Lang.Object /
/// Java.Lang.Throwable). Such types cannot be marshaled and produce XA4212. Interfaces and
/// System.Exception subclasses are excluded, matching the legacy XAJavaTypeScanner.
/// </summary>
bool IsCustomJavaObject (TypeDefinition typeDef, AssemblyIndex index)
{
if ((typeDef.Attributes & TypeAttributes.Interface) != 0) {
return false;
}
if (!ImplementsIJavaObject (typeDef, index)) {
return false;
}
if (IsSubclassOfSystemException (typeDef, index)) {
return false;
}
return true;
}
Comment thread
Copilot marked this conversation as resolved.

/// <summary>
/// Check whether a type implements Android.Runtime.IJavaObject, directly or through an
/// interface that extends it, or via a base class. Results are cached; false-before-recurse
/// prevents cycles.
/// </summary>
bool ImplementsIJavaObject (TypeDefinition typeDef, AssemblyIndex index)
{
var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader);
var key = $"{index.AssemblyName}:{fullName}";

if (implementsIJavaObjectCache.TryGetValue (key, out var cached)) {
return cached;
}

// Mark as false to prevent cycles, then compute
implementsIJavaObjectCache [key] = false;

foreach (var implHandle in typeDef.GetInterfaceImplementations ()) {
var impl = index.Reader.GetInterfaceImplementation (implHandle);
var resolved = ResolveEntityHandle (impl.Interface, index);
if (resolved is null) {
continue;
}

if (resolved.ManagedTypeName == IJavaObjectFullName) {
implementsIJavaObjectCache [key] = true;
return true;
}

// Recurse into the interface's own base interfaces
if (TryResolveType (resolved.ManagedTypeName, resolved.AssemblyName, out var ifaceHandle, out var ifaceIndex)) {
var ifaceDef = ifaceIndex.Reader.GetTypeDefinition (ifaceHandle);
if (ImplementsIJavaObject (ifaceDef, ifaceIndex)) {
implementsIJavaObjectCache [key] = true;
return true;
}
}
}

// Walk the base class chain
var baseInfo = GetBaseTypeInfo (typeDef, index);
if (baseInfo is not null &&
TryResolveType (baseInfo.ManagedTypeName, baseInfo.AssemblyName, out var baseHandle, out var baseIndex)) {
var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle);
if (ImplementsIJavaObject (baseDef, baseIndex)) {
implementsIJavaObjectCache [key] = true;
return true;
}
}

return false;
}

/// <summary>
/// Walk the base type chain to determine whether the type derives from System.Exception.
/// </summary>
bool IsSubclassOfSystemException (TypeDefinition typeDef, AssemblyIndex index)
{
var baseInfo = GetBaseTypeInfo (typeDef, index);
int guard = 0;
while (baseInfo is not null && guard++ < 256) {
if (baseInfo.ManagedTypeName == "System.Exception") {
return true;
}
if (!TryResolveType (baseInfo.ManagedTypeName, baseInfo.AssemblyName, out var baseHandle, out var baseIndex)) {
return false;
}
var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle);
baseInfo = GetBaseTypeInfo (baseDef, baseIndex);
}
return false;
}

/// <summary>
/// Check if a type extends a known Java peer (has [Register] or component attribute)
/// by walking the base type chain. Results are cached; false-before-recurse prevents cycles.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,13 @@ public TrimmableTypeMapResult Execute (
ManifestConfig? manifestConfig = null,
XDocument? manifestTemplate = null,
string? packageNamingPolicy = null,
bool generateTypeMapAssemblies = true)
bool generateTypeMapAssemblies = true,
bool errorOnCustomJavaObject = true)
{
_ = assemblies ?? throw new ArgumentNullException (nameof (assemblies));
_ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
_ = frameworkAssemblyNames ?? throw new ArgumentNullException (nameof (frameworkAssemblyNames));
var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy, frameworkAssemblyNames);
var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy, frameworkAssemblyNames, errorOnCustomJavaObject);
if (allPeers.Count == 0) {
logger.LogNoJavaPeerTypesFound ();
return new TrimmableTypeMapResult ([], [], allPeers);
Expand Down Expand Up @@ -161,9 +162,10 @@ GeneratedManifest GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifes
(List<JavaPeerInfo> peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (
IReadOnlyList<AssemblyInput> assemblies,
string? packageNamingPolicy,
HashSet<string> frameworkAssemblyNames)
HashSet<string> frameworkAssemblyNames,
bool errorOnCustomJavaObject = true)
{
using var scanner = new JavaPeerScanner (packageNamingPolicy, logger, frameworkAssemblyNames);
using var scanner = new JavaPeerScanner (packageNamingPolicy, logger, frameworkAssemblyNames, errorOnCustomJavaObject);
var peers = scanner.Scan (assemblies);
var manifestInfo = scanner.ScanAssemblyManifestInfo ();
logger.LogJavaPeerScanInfo (assemblies.Count, peers.Count);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
ManifestPlaceholders="$(AndroidManifestPlaceholders)"
CheckedBuild="$(_AndroidCheckedBuild)"
ApplicationJavaClass="$(AndroidApplicationJavaClass)"
ErrorOnCustomJavaObject="$(AndroidErrorOnCustomJavaObject)"
GeneratedAssembliesListFile="$(_TypeMapAssembliesListFile)"
AcwMapOutputFile="$(IntermediateOutputPath)acw-map.txt"
ApplicationRegistrationOutputFile="$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java">
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/Xamarin.Android.Build.Tasks/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,11 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla
<data name="XA4213" xml:space="preserve">
<value>The type '{0}' must provide a public default constructor</value>
</data>
<data name="XA4212" xml:space="preserve">
<value>Type `{0}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported.</value>
<comment>The following are literal names and should not be translated: Android.Runtime.IJavaObject, Java.Lang.Object, Java.Lang.Throwable
{0} - The managed type name</comment>
</data>
<data name="XA4214" xml:space="preserve">
<value>The managed type `{0}` exists in multiple assemblies: {1}. Please refactor the managed type names in these assemblies so that they are not identical.</value>
<comment>{0} - The managed type name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ public void LogUnresolvableJavaPeerSkippedWarning (
log.LogCodedWarning ("XA4257", Properties.Resources.XA4257, managedTypeName, assemblyName, unresolvedTypeName, unresolvedAssemblyName, unresolvedAssemblyPath);
public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) =>
log.LogCodedError ("XA4251", Properties.Resources.XA4251, managedTypeName);
public void LogCustomJavaObjectError (string managedTypeName) =>
log.LogError ("{0}", $"XA4212: {string.Format (Properties.Resources.XA4212, managedTypeName)}");
public void LogCustomJavaObjectWarning (string managedTypeName) =>
log.LogWarning ("{0}", $"XA4212: {string.Format (Properties.Resources.XA4212, managedTypeName)}");
}

public override string TaskPrefix => "GTT";
Expand Down Expand Up @@ -108,6 +112,13 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN
public bool GenerateTypeMapAssemblies { get; set; } = true;
public bool CleanJavaSourceOutputDirectory { get; set; }

/// <summary>
/// When true (the default, from <c>$(AndroidErrorOnCustomJavaObject)</c>), a managed class
/// that implements <c>Android.Runtime.IJavaObject</c> without deriving from a Java peer is
/// reported as the XA4212 error; otherwise it is reported as a warning.
/// </summary>
public bool ErrorOnCustomJavaObject { get; set; } = true;

[Output]
public ITaskItem [] GeneratedAssemblies { get; set; } = [];
[Output]
Expand Down Expand Up @@ -207,7 +218,8 @@ public override bool RunTask ()
manifestConfig: manifestConfig,
manifestTemplate: manifestTemplate,
packageNamingPolicy: PackageNamingPolicy,
generateTypeMapAssemblies: GenerateTypeMapAssemblies);
generateTypeMapAssemblies: GenerateTypeMapAssemblies,
errorOnCustomJavaObject: ErrorOnCustomJavaObject);

if (GenerateTypeMapAssemblies) {
GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1145,12 +1145,6 @@ public void XA4212 ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)]
if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) {
return;
}
// TODO: the trimmable typemap generator (the NativeAOT default) does not yet emit the
// XA4212 custom-IJavaObject diagnostic that the managed/llvm-ir typemap paths raise via
// XAJavaTypeScanner. Re-enable once that detection is added to TrimmableTypeMapGenerator.
if (IgnoreOnNativeAot (runtime, "the trimmable typemap does not yet emit the XA4212 custom-IJavaObject diagnostic (tracked as a follow-up).")) {
return;
}

var proj = new XamarinAndroidApplicationProject () {
IsRelease = isRelease,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ public void LogUnresolvableJavaPeerSkippedWarning (
$"'{unresolvedTypeName}' from '{unresolvedAssemblyName}' at '{unresolvedAssemblyPath}' could not be resolved.");
public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) =>
logMessages.Add ($"XA4251: Type '{managedTypeName}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map.");
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) =>
warnings?.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported.");
}

[Fact]
Expand Down