Skip to content

[Mono.Android] broaden assignability check escape hatch#11606

Draft
simonrozsival wants to merge 4 commits into
mainfrom
dev/srozsival/broaden-assignable-from-check
Draft

[Mono.Android] broaden assignability check escape hatch#11606
simonrozsival wants to merge 4 commits into
mainfrom
dev/srozsival/broaden-assignable-from-check

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Extend Microsoft.Android.Runtime.RuntimeFeature.IsAssignableFromCheck to CoreCLR peer creation paths in JavaMarshalValueManager.
  • Keep the default behavior unchanged, but when _AndroidIsAssignableFromCheck=false, skip Java assignability checks (IsAssignableFrom) so peer creation can continue without running the check.
  • Keep the implementation focused on specific assignability decision points (no duplicated feature-specific peer-creation flow), and use standalone if (RuntimeFeature.IsX) blocks for trimmer-friendly branching.
  • Add coverage for wrapping a remapped LayoutInflater returned from Context.LayoutInflaterService, and add a CoreCLR Intune assignability package-test leg.

Motivation

dotnet/android#10475 introduced _AndroidIsAssignableFromCheck=false for the Mono.Android typemap path. In MAUI + Intune remapping scenarios, LayoutInflater.From(Context) can still return null through the CoreCLR/Java.Interop peer creation path, which performs a separate assignability check not covered by the existing switch.

This draft explores making the switch consistently mean: when explicitly disabled, Java remapping-induced assignability mismatches should not reject managed peer creation.

Validation

  • MSBUILDDISABLENODEREUSE=1 ./dotnet-local.sh build src/Mono.Android/Mono.Android.csproj --no-restore -m:1 -v:minimal
  • ./dotnet-local.sh build tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj -f net11.0-android -p:TestsFlavor=CoreCLRIsAssignableFrom -p:IncludeCategories=Intune -p:_AndroidIsAssignableFromCheck=false -p:UseMonoRuntime=false -m:1 -v:minimal ⚠️ blocked locally with XA5300 because the Android SDK directory is not available in this environment.

Honor Microsoft.Android.Runtime.RuntimeFeature.IsAssignableFromCheck in CoreCLR peer creation paths so remapped Java types can still be wrapped when the escape hatch is disabled.

Add coverage for wrapping a remapped LayoutInflater service and run the Intune assignability tests under CoreCLR.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines successfully started running 1 pipeline(s).

@simonrozsival simonrozsival left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot address this code review

}
var isAssignable = JniEnvironment.Types.IsAssignableFrom (objClass, targetClass);
return isAssignable ? proxy : null;
if (!isAssignable && RuntimeFeature.IsAssignableFromCheck) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot RuntimeFeature checks should be in a standalone if (RuntimeFeature.IsX) block so that the trimmer can reliably identify it and trim the unreachable codepath.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 01852f9: RuntimeFeature checks are now in standalone if (RuntimeFeature.IsAssignableFromCheck) blocks for trimmer-friendly flow.

@@ -585,8 +717,12 @@ static bool IsIncompatibleCast (
}

if (!JniEnvironment.Types.IsAssignableFrom (instanceClass, targetClass)) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot I think the right way to handle this is NOT to call IsAssignableFrom at all when RuntimeFeature.IsAssignableFromCheck is false. That's much easier to follow and it doesn't do unnecessary work when the feature is disabled.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 01852f9: IsAssignableFrom is no longer called when RuntimeFeature.IsAssignableFromCheck is false.

Comment on lines +550 to +677
IJavaPeerable? CreatePeerAllowingIncompatibleJavaType (
ref JniObjectReference reference,
JniObjectReferenceOptions transfer,
[DynamicallyAccessedMembers (Constructors)]
Type? targetType)
{
// Mirrors Java.Interop's CreatePeer path, but lets the Android assignability
// feature switch decide whether a Java type mismatch rejects peer creation.
var resolvedTargetType = ResolvePeerType (targetType ?? typeof (global::Java.Interop.JavaObject));
if (resolvedTargetType is null) {
return null;
}

if (!typeof (IJavaPeerable).IsAssignableFrom (resolvedTargetType)) {
throw new ArgumentException ($"targetType `{resolvedTargetType.AssemblyQualifiedName}` must implement IJavaPeerable!", nameof (targetType));
}

var targetSig = Runtime.TypeManager.GetTypeSignature (resolvedTargetType);
if (!targetSig.IsValid || targetSig.SimpleReference == null) {
throw new ArgumentException ($"Could not determine Java type corresponding to `{resolvedTargetType.AssemblyQualifiedName}`.", nameof (targetType));
}

var refClass = JniEnvironment.Types.GetObjectClass (reference);
JniObjectReference targetClass;
try {
targetClass = JniEnvironment.Types.FindClass (targetSig.SimpleReference);
} catch (Exception e) {
JniObjectReference.Dispose (ref refClass);
throw new ArgumentException ($"Could not find Java class `{targetSig.SimpleReference}`.",
nameof (targetType),
e);
}

if (!JniEnvironment.Types.IsAssignableFrom (refClass, targetClass) && Logger.LogAssembly) {
var message = $"Handle 0x{reference.Handle:x} is of type '{JniEnvironment.Types.GetJniTypeNameFromInstance (reference)}' which is not assignable to '{targetSig.SimpleReference}'";
Logger.Log (LogLevel.Debug, "monodroid-assembly", message);
}

JniObjectReference.Dispose (ref targetClass);

var peer = CreatePeerInstance (ref refClass, resolvedTargetType, ref reference, transfer);
if (peer == null) {
throw new NotSupportedException (string.Format (CultureInfo.InvariantCulture, "Could not find an appropriate constructable wrapper type for Java type '{0}', targetType='{1}'.",
JniEnvironment.Types.GetJniTypeNameFromInstance (reference), resolvedTargetType));
}
peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable);
return peer;
}

IJavaPeerable? CreatePeerInstance (
ref JniObjectReference klass,
[DynamicallyAccessedMembers (Constructors)]
Type targetType,
ref JniObjectReference reference,
JniObjectReferenceOptions transfer)
{
var jniTypeName = JniEnvironment.Types.GetJniTypeNameFromClass (klass);

while (jniTypeName != null) {
if (!JniTypeSignature.TryParse (jniTypeName, out var sig)) {
return null;
}

Type? type = GetTypeAssignableTo (sig, targetType);
if (type != null) {
var peer = TryCreatePeerInstance (ref reference, transfer, type);

if (peer != null) {
JniObjectReference.Dispose (ref klass);
return peer;
}
}

var super = JniEnvironment.Types.GetSuperclass (klass);
jniTypeName = super.IsValid
? JniEnvironment.Types.GetJniTypeNameFromClass (super)
: null;

JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose);
klass = super;
}
JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose);

return TryCreatePeerInstance (ref reference, transfer, targetType);

[UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "Types returned here should be preserved via other means.")]
[return: DynamicallyAccessedMembers (Constructors)]
Type? GetTypeAssignableTo (JniTypeSignature sig, Type targetType)
{
foreach (var type in Runtime.TypeManager.GetTypes (sig)) {
if (targetType.IsAssignableFrom (type)) {
return type;
}
}
return null;
}
}

IJavaPeerable? TryCreatePeerInstance (
ref JniObjectReference reference,
JniObjectReferenceOptions options,
[DynamicallyAccessedMembers (Constructors)]
Type type)
{
type = Runtime.TypeManager.GetInvokerType (type) ?? type;

var self = GetUninitializedObject (type);
var constructed = false;
try {
constructed = TryConstructPeer (self, ref reference, options, type);
} finally {
if (!constructed) {
GC.SuppressFinalize (self);
self = null;
}
}
return self;

static IJavaPeerable GetUninitializedObject (
[DynamicallyAccessedMembers (Constructors)]
Type type)
{
var value = (IJavaPeerable) RuntimeHelpers.GetUninitializedObject (type);
value.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable);
return value;
}
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely not the right way to implement this. I'm 100% sure we don't want to duplicate and significantly diverge the logic in this class based on this feature switch. We need only a very specific location changed with an if - else

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 01852f9: removed the feature-specific duplicated class-level flow and narrowed the change to the assignability decision point via a focused if/else in CreatePeer.

Copilot AI and others added 3 commits June 8, 2026 13:57
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>

Copilot AI commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

@copilot address this code review

Done in 01852f9 (plus follow-up fixups): I addressed the @copilot review feedback in JavaMarshalValueManager and TrimmableTypeMap, and revalidated the updated changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants