Skip to content

Commit ea3c37b

Browse files
[NativeAOT] Emit XA4212 for custom IJavaObject types on the trimmable path
Root-cause fix (replaces the earlier test skip). The XA4212 "custom IJavaObject not supported" diagnostic was produced only by the Cecil-based XAJavaTypeScanner, which runs on the managed path (AssemblyModifierPipeline in _RunAfterILLinkAdditionalSteps) and the llvm-ir path. On the trimmable typemap path — now the NativeAOT default — that post-ILLink pipeline is intentionally skipped as dead work, so a managed class implementing Android.Runtime.IJavaObject without deriving from Java.Lang.Object/Throwable was silently ignored: no diagnostic, and the type is absent from the typemap, failing at runtime. Detect this case directly in JavaPeerScanner, which already scans every type: a class that is not a Java peer, is not a System.Exception subclass, and implements Android.Runtime.IJavaObject (directly, transitively via an interface, or through a base class) now emits XA4212. Thread AndroidErrorOnCustomJavaObject through the GenerateTrimmableTypeMap task and generator so it errors by default and warns when disabled, matching the legacy behavior and message format. Un-skip the XA4212 test. Verified locally: XA4212(NativeAOT) and XA4212(CoreCLR) pass, 614 trimmable-typemap generator unit tests pass, and BuildBasicApplicationAppCompat (NativeAOT + CoreCLR) builds clean with no false-positive XA4212. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 94f450c7-781d-4019-a56c-fae1c456b9a7
1 parent bd785e3 commit ea3c37b

9 files changed

Lines changed: 148 additions & 12 deletions

File tree

src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ void LogUnresolvableJavaPeerSkippedWarning (
2121
string unresolvedAssemblyName,
2222
string unresolvedAssemblyPath);
2323
void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName);
24+
void LogCustomJavaObjectError (string managedTypeName);
25+
void LogCustomJavaObjectWarning (string managedTypeName);
2426
}

src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ enum HashedPackageNamingPolicy {
3232
readonly ITrimmableTypeMapLogger? logger;
3333
readonly HashedPackageNamingPolicy packageNamingPolicy;
3434
readonly HashSet<string> frameworkAssemblyNames;
35+
readonly bool errorOnCustomJavaObject;
3536

36-
public JavaPeerScanner (string? packageNamingPolicy = null, ITrimmableTypeMapLogger? logger = null, HashSet<string>? frameworkAssemblyNames = null)
37+
public JavaPeerScanner (string? packageNamingPolicy = null, ITrimmableTypeMapLogger? logger = null, HashSet<string>? frameworkAssemblyNames = null, bool errorOnCustomJavaObject = false)
3738
{
3839
this.packageNamingPolicy = ParsePackageNamingPolicy (packageNamingPolicy);
3940
this.logger = logger;
4041
this.frameworkAssemblyNames = frameworkAssemblyNames ?? new HashSet<string> (StringComparer.OrdinalIgnoreCase);
42+
this.errorOnCustomJavaObject = errorOnCustomJavaObject;
4143
}
4244

4345
/// <summary>
@@ -338,6 +340,17 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
338340
if (ExtendsJavaPeer (typeDef, index)) {
339341
(jniName, compatJniName) = ComputeAutoJniNames (typeDef, index);
340342
} else {
343+
// A managed class that implements Android.Runtime.IJavaObject but does not
344+
// derive from a Java peer (Java.Lang.Object / Java.Lang.Throwable) cannot be
345+
// marshaled to Java. Mirror the legacy XAJavaTypeScanner XA4212 diagnostic,
346+
// which the managed/llvm-ir typemap paths raise via GenerateJavaStubs.
347+
if (IsCustomJavaObject (typeDef, index)) {
348+
if (errorOnCustomJavaObject) {
349+
logger?.LogCustomJavaObjectError (fullName);
350+
} else {
351+
logger?.LogCustomJavaObjectWarning (fullName);
352+
}
353+
}
341354
continue;
342355
}
343356
}
@@ -2151,6 +2164,100 @@ public void Dispose ()
21512164

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

2167+
const string IJavaObjectFullName = "Android.Runtime.IJavaObject";
2168+
2169+
readonly Dictionary<string, bool> implementsIJavaObjectCache = new (StringComparer.Ordinal);
2170+
2171+
/// <summary>
2172+
/// Determines whether a type is a "custom" Java object: a managed class that implements
2173+
/// Android.Runtime.IJavaObject but does not derive from a Java peer (Java.Lang.Object /
2174+
/// Java.Lang.Throwable). Such types cannot be marshaled and produce XA4212. Interfaces and
2175+
/// System.Exception subclasses are excluded, matching the legacy XAJavaTypeScanner.
2176+
/// </summary>
2177+
bool IsCustomJavaObject (TypeDefinition typeDef, AssemblyIndex index)
2178+
{
2179+
if ((typeDef.Attributes & TypeAttributes.Interface) != 0) {
2180+
return false;
2181+
}
2182+
if (IsSubclassOfSystemException (typeDef, index)) {
2183+
return false;
2184+
}
2185+
return ImplementsIJavaObject (typeDef, index);
2186+
}
2187+
2188+
/// <summary>
2189+
/// Check whether a type implements Android.Runtime.IJavaObject, directly or through an
2190+
/// interface that extends it, or via a base class. Results are cached; false-before-recurse
2191+
/// prevents cycles.
2192+
/// </summary>
2193+
bool ImplementsIJavaObject (TypeDefinition typeDef, AssemblyIndex index)
2194+
{
2195+
var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader);
2196+
var key = $"{index.AssemblyName}:{fullName}";
2197+
2198+
if (implementsIJavaObjectCache.TryGetValue (key, out var cached)) {
2199+
return cached;
2200+
}
2201+
2202+
// Mark as false to prevent cycles, then compute
2203+
implementsIJavaObjectCache [key] = false;
2204+
2205+
foreach (var implHandle in typeDef.GetInterfaceImplementations ()) {
2206+
var impl = index.Reader.GetInterfaceImplementation (implHandle);
2207+
var resolved = ResolveEntityHandle (impl.Interface, index);
2208+
if (resolved is null) {
2209+
continue;
2210+
}
2211+
2212+
if (resolved.ManagedTypeName == IJavaObjectFullName) {
2213+
implementsIJavaObjectCache [key] = true;
2214+
return true;
2215+
}
2216+
2217+
// Recurse into the interface's own base interfaces
2218+
if (TryResolveType (resolved.ManagedTypeName, resolved.AssemblyName, out var ifaceHandle, out var ifaceIndex)) {
2219+
var ifaceDef = ifaceIndex.Reader.GetTypeDefinition (ifaceHandle);
2220+
if (ImplementsIJavaObject (ifaceDef, ifaceIndex)) {
2221+
implementsIJavaObjectCache [key] = true;
2222+
return true;
2223+
}
2224+
}
2225+
}
2226+
2227+
// Walk the base class chain
2228+
var baseInfo = GetBaseTypeInfo (typeDef, index);
2229+
if (baseInfo is not null &&
2230+
TryResolveType (baseInfo.ManagedTypeName, baseInfo.AssemblyName, out var baseHandle, out var baseIndex)) {
2231+
var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle);
2232+
if (ImplementsIJavaObject (baseDef, baseIndex)) {
2233+
implementsIJavaObjectCache [key] = true;
2234+
return true;
2235+
}
2236+
}
2237+
2238+
return false;
2239+
}
2240+
2241+
/// <summary>
2242+
/// Walk the base type chain to determine whether the type derives from System.Exception.
2243+
/// </summary>
2244+
bool IsSubclassOfSystemException (TypeDefinition typeDef, AssemblyIndex index)
2245+
{
2246+
var baseInfo = GetBaseTypeInfo (typeDef, index);
2247+
int guard = 0;
2248+
while (baseInfo is not null && guard++ < 256) {
2249+
if (baseInfo.ManagedTypeName == "System.Exception") {
2250+
return true;
2251+
}
2252+
if (!TryResolveType (baseInfo.ManagedTypeName, baseInfo.AssemblyName, out var baseHandle, out var baseIndex)) {
2253+
return false;
2254+
}
2255+
var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle);
2256+
baseInfo = GetBaseTypeInfo (baseDef, baseIndex);
2257+
}
2258+
return false;
2259+
}
2260+
21542261
/// <summary>
21552262
/// Check if a type extends a known Java peer (has [Register] or component attribute)
21562263
/// by walking the base type chain. Results are cached; false-before-recurse prevents cycles.

src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ public TrimmableTypeMapResult Execute (
3535
XDocument? manifestTemplate = null,
3636
string? packageNamingPolicy = null,
3737
int maxArrayRank = 0,
38-
bool generateTypeMapAssemblies = true)
38+
bool generateTypeMapAssemblies = true,
39+
bool errorOnCustomJavaObject = false)
3940
{
4041
_ = assemblies ?? throw new ArgumentNullException (nameof (assemblies));
4142
_ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion));
@@ -44,7 +45,7 @@ public TrimmableTypeMapResult Execute (
4445
throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, "Must be >= 0.");
4546
}
4647

47-
var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy, frameworkAssemblyNames);
48+
var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy, frameworkAssemblyNames, errorOnCustomJavaObject);
4849
if (allPeers.Count == 0) {
4950
logger.LogNoJavaPeerTypesFound ();
5051
return new TrimmableTypeMapResult ([], [], allPeers);
@@ -166,9 +167,10 @@ GeneratedManifest GenerateManifest (List<JavaPeerInfo> allPeers, AssemblyManifes
166167
(List<JavaPeerInfo> peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (
167168
IReadOnlyList<AssemblyInput> assemblies,
168169
string? packageNamingPolicy,
169-
HashSet<string> frameworkAssemblyNames)
170+
HashSet<string> frameworkAssemblyNames,
171+
bool errorOnCustomJavaObject = false)
170172
{
171-
using var scanner = new JavaPeerScanner (packageNamingPolicy, logger, frameworkAssemblyNames);
173+
using var scanner = new JavaPeerScanner (packageNamingPolicy, logger, frameworkAssemblyNames, errorOnCustomJavaObject);
172174
var peers = scanner.Scan (assemblies);
173175
var manifestInfo = scanner.ScanAssemblyManifestInfo ();
174176
logger.LogJavaPeerScanInfo (assemblies.Count, peers.Count);

src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@
156156
ManifestPlaceholders="$(AndroidManifestPlaceholders)"
157157
CheckedBuild="$(_AndroidCheckedBuild)"
158158
ApplicationJavaClass="$(AndroidApplicationJavaClass)"
159+
ErrorOnCustomJavaObject="$(AndroidErrorOnCustomJavaObject)"
159160
GeneratedAssembliesListFile="$(_TypeMapAssembliesListFile)"
160161
AcwMapOutputFile="$(IntermediateOutputPath)acw-map.txt"
161162
ApplicationRegistrationOutputFile="$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java">

src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Xamarin.Android.Build.Tasks/Properties/Resources.resx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,11 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla
603603
<data name="XA4213" xml:space="preserve">
604604
<value>The type '{0}' must provide a public default constructor</value>
605605
</data>
606+
<data name="XA4212" xml:space="preserve">
607+
<value>Type `{0}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported.</value>
608+
<comment>The following are literal names and should not be translated: Android.Runtime.IJavaObject, Java.Lang.Object, Java.Lang.Throwable
609+
{0} - The managed type name</comment>
610+
</data>
606611
<data name="XA4214" xml:space="preserve">
607612
<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>
608613
<comment>{0} - The managed type name

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ public void LogUnresolvableJavaPeerSkippedWarning (
5757
log.LogCodedWarning ("XA4257", Properties.Resources.XA4257, managedTypeName, assemblyName, unresolvedTypeName, unresolvedAssemblyName, unresolvedAssemblyPath);
5858
public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) =>
5959
log.LogCodedError ("XA4251", Properties.Resources.XA4251, managedTypeName);
60+
public void LogCustomJavaObjectError (string managedTypeName) =>
61+
log.LogError ("{0}", $"XA4212: {string.Format (Properties.Resources.XA4212, managedTypeName)}");
62+
public void LogCustomJavaObjectWarning (string managedTypeName) =>
63+
log.LogWarning ("{0}", $"XA4212: {string.Format (Properties.Resources.XA4212, managedTypeName)}");
6064
}
6165

6266
public override string TaskPrefix => "GTT";
@@ -115,6 +119,13 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN
115119
public bool GenerateTypeMapAssemblies { get; set; } = true;
116120
public bool CleanJavaSourceOutputDirectory { get; set; }
117121

122+
/// <summary>
123+
/// When true (the default, from <c>$(AndroidErrorOnCustomJavaObject)</c>), a managed class
124+
/// that implements <c>Android.Runtime.IJavaObject</c> without deriving from a Java peer is
125+
/// reported as the XA4212 error; otherwise it is reported as a warning.
126+
/// </summary>
127+
public bool ErrorOnCustomJavaObject { get; set; } = true;
128+
118129
[Output]
119130
public ITaskItem [] GeneratedAssemblies { get; set; } = [];
120131
[Output]
@@ -215,7 +226,8 @@ public override bool RunTask ()
215226
manifestTemplate: manifestTemplate,
216227
packageNamingPolicy: PackageNamingPolicy,
217228
maxArrayRank: MaxArrayRank,
218-
generateTypeMapAssemblies: GenerateTypeMapAssemblies);
229+
generateTypeMapAssemblies: GenerateTypeMapAssemblies,
230+
errorOnCustomJavaObject: ErrorOnCustomJavaObject);
219231

220232
if (GenerateTypeMapAssemblies) {
221233
GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ());

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,12 +1059,6 @@ public void XA4212 ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)]
10591059
if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) {
10601060
return;
10611061
}
1062-
// TODO: the trimmable typemap generator (the NativeAOT default) does not yet emit the
1063-
// XA4212 custom-IJavaObject diagnostic that the managed/llvm-ir typemap paths raise via
1064-
// XAJavaTypeScanner. Re-enable once that detection is added to TrimmableTypeMapGenerator.
1065-
if (IgnoreOnNativeAot (runtime, "the trimmable typemap does not yet emit the XA4212 custom-IJavaObject diagnostic (tracked as a follow-up).")) {
1066-
return;
1067-
}
10681062

10691063
var proj = new XamarinAndroidApplicationProject () {
10701064
IsRelease = isRelease,

tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ public void LogUnresolvableJavaPeerSkippedWarning (
5151
$"'{unresolvedTypeName}' from '{unresolvedAssemblyName}' at '{unresolvedAssemblyPath}' could not be resolved.");
5252
public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) =>
5353
logMessages.Add ($"XA4251: Type '{managedTypeName}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map.");
54+
public void LogCustomJavaObjectError (string managedTypeName) =>
55+
warnings?.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported.");
56+
public void LogCustomJavaObjectWarning (string managedTypeName) =>
57+
warnings?.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported.");
5458
}
5559

5660
[Fact]

0 commit comments

Comments
 (0)