[Mono.Android] broaden assignability check escape hatch#11606
[Mono.Android] broaden assignability check escape hatch#11606simonrozsival wants to merge 4 commits into
Conversation
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>
|
/azp run |
|
Azure Pipelines successfully started running 1 pipeline(s). |
simonrozsival
left a comment
There was a problem hiding this comment.
@copilot address this code review
| } | ||
| var isAssignable = JniEnvironment.Types.IsAssignableFrom (objClass, targetClass); | ||
| return isAssignable ? proxy : null; | ||
| if (!isAssignable && RuntimeFeature.IsAssignableFromCheck) { |
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
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)) { | |||
There was a problem hiding this comment.
@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.
There was a problem hiding this comment.
Addressed in 01852f9: IsAssignableFrom is no longer called when RuntimeFeature.IsAssignableFromCheck is false.
| 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; | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
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>
Summary
Microsoft.Android.Runtime.RuntimeFeature.IsAssignableFromCheckto CoreCLR peer creation paths inJavaMarshalValueManager._AndroidIsAssignableFromCheck=false, skip Java assignability checks (IsAssignableFrom) so peer creation can continue without running the check.if (RuntimeFeature.IsX)blocks for trimmer-friendly branching.LayoutInflaterreturned fromContext.LayoutInflaterService, and add a CoreCLR Intune assignability package-test leg.Motivation
dotnet/android#10475introduced_AndroidIsAssignableFromCheck=falsefor 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:minimalXA5300because the Android SDK directory is not available in this environment.