diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 9c0867c0875..93f8ce5c5fc 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -41,6 +41,7 @@ public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) writer.Write (proxy.TargetType.ManagedTypeName); writer.Write (proxy.TargetType.AssemblyName); writer.Write ((byte)(proxy.ActivationCtor?.Style ?? 0)); + writer.Write ((byte)(proxy.InvokerActivationCtorStyle ?? 0)); } foreach (var assoc in data.Associations) { writer.Write (assoc.SourceTypeReference); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 679423576f2..5f945a8ac05 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -110,6 +110,11 @@ sealed class JavaPeerProxyData /// public TypeRefData? InvokerType { get; set; } + /// + /// Activation constructor style to use when creating . + /// + public ActivationCtorStyle? InvokerActivationCtorStyle { get; set; } + /// /// Whether this proxy has a CreateInstance that can actually create instances. /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 79570a14bda..7439f68173a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -277,6 +277,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, Hash ManagedTypeName = peer.InvokerTypeName, AssemblyName = peer.AssemblyName, }; + proxy.InvokerActivationCtorStyle = peer.InvokerActivationCtorStyle ?? ActivationCtorStyle.XamarinAndroid; } if (peer.ActivationCtor != null) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 417b1096f60..d489edcdc07 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -604,25 +604,28 @@ void EmitCreateInstance (JavaPeerProxyData proxy) return; } - // JavaInterop-style activation ctors (ref JniObjectReference, JniObjectReferenceOptions) - // require parameter conversion from (IntPtr, JniHandleOwnership). - if (proxy.ActivationCtor?.Style == ActivationCtorStyle.JavaInterop) { - if (proxy.InvokerType != null) { - EmitCreateInstanceViaJavaInteropNewobj (_pe.ResolveTypeRef (proxy.InvokerType)); + if (proxy.InvokerType != null) { + var invokerType = _pe.ResolveTypeRef (proxy.InvokerType); + if (proxy.InvokerActivationCtorStyle == ActivationCtorStyle.JavaInterop) { + EmitCreateInstanceViaJavaInteropNewobj (invokerType); } else { - var targetRef = _pe.ResolveTypeRef (proxy.TargetType); - var jiCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ("ActivationCtor should not be null"); - if (jiCtor.IsOnLeafType) { - EmitCreateInstanceViaJavaInteropNewobj (targetRef); - } else { - EmitCreateInstanceInheritedJavaInteropCtor (targetRef, jiCtor); - } + EmitCreateInstanceViaNewobj (invokerType); } return; } - if (proxy.InvokerType != null) { - EmitCreateInstanceViaNewobj (_pe.ResolveTypeRef (proxy.InvokerType)); + // JavaInterop-style activation ctors (ref JniObjectReference, JniObjectReferenceOptions) + // require parameter conversion from (IntPtr, JniHandleOwnership). + if (proxy.ActivationCtor?.Style == ActivationCtorStyle.JavaInterop) { + var targetRef = _pe.ResolveTypeRef (proxy.TargetType); + var jiCtor = proxy.ActivationCtor ?? throw new InvalidOperationException ("ActivationCtor should not be null"); + if (jiCtor.IsOnLeafType) { + EmitCreateInstanceViaJavaInteropNewobj (targetRef); + } else { + // Legacy GetConstructor() doesn't find inherited ctors — + // match that behavior by returning null. + EmitCreateInstanceNoActivation (); + } return; } @@ -633,7 +636,9 @@ void EmitCreateInstance (JavaPeerProxyData proxy) if (activationCtor.IsOnLeafType) { EmitCreateInstanceViaNewobj (targetTypeRef); } else { - EmitCreateInstanceInheritedCtor (targetTypeRef, activationCtor); + // Legacy GetConstructor() doesn't find inherited ctors — + // match that behavior by returning null. + EmitCreateInstanceNoActivation (); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 1db8dfd8309..9bf542f6b7b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -235,10 +235,13 @@ internal RegisterInfo ParseJniTypeSignatureAttribute (CustomAttribute ca) doNotGenerateAcw = !generateJavaPeer; } + var isArrayType = TryGetNamedArgument (value, "ArrayRank", out var rank) && rank > 0; + return new RegisterInfo { JniName = jniName.Replace ('.', '/'), DoNotGenerateAcw = doNotGenerateAcw, IsFromJniTypeSignature = true, + IsArrayType = isArrayType, }; } @@ -529,6 +532,7 @@ sealed record RegisterInfo public string? Connector { get; init; } public bool DoNotGenerateAcw { get; init; } public bool IsFromJniTypeSignature { get; init; } + public bool IsArrayType { get; init; } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index bcc45d1b1c5..ee285b42798 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -125,6 +125,13 @@ public sealed record JavaPeerInfo /// public string? InvokerTypeName { get; init; } + /// + /// Activation constructor style declared by . + /// Kept separate from , which describes the + /// target type or its base types. + /// + public ActivationCtorStyle? InvokerActivationCtorStyle { get; init; } + /// /// True if this is an open generic type definition. /// Generic types get TypeMap entries but CreateInstance throws NotSupportedException. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 2e09766a2fa..8b32abf3940 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -182,6 +182,15 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A index.AttributesByType.TryGetValue (typeHandle, out var attrInfo); if (registerInfo is not null && !string.IsNullOrEmpty (registerInfo.JniName)) { + // [JniTypeSignature] with ArrayRank > 0 represents a JNI array wrapper + // (e.g., JavaBooleanArray, JavaObjectArray, JavaPrimitiveArray). + // These are handled by the built-in tables in JniRuntime.JniTypeManager + // and must not be added to the typemap — keyword types (Z, B, etc.) + // would collide with GetPrimitiveArrayTypesForSimpleReference, and + // non-keyword array types would add unnecessary aliases. + if (registerInfo.IsArrayType) { + continue; + } jniName = registerInfo.JniName; compatJniName = jniName; doNotGenerateAcw = registerInfo.DoNotGenerateAcw; @@ -208,6 +217,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A var isUnconditional = attrInfo is not null; var cannotRegisterInStaticConstructor = attrInfo is ApplicationAttributeInfo or InstrumentationAttributeInfo; string? invokerTypeName = null; + ActivationCtorStyle? invokerActivationCtorStyle = null; // Resolve base Java type name var baseJavaName = ResolveBaseJavaName (typeDef, index, results); @@ -229,6 +239,13 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A invokerTypeName = TryFindInvokerTypeName (fullName, typeHandle, index); } + // Interface/abstract peers create their invoker, not the target type. + // Keep ActivationCtor scoped to the target/base hierarchy for legacy parity, + // and store the invoker ctor style separately for CreateInstance emission. + if (invokerTypeName is not null) { + invokerActivationCtorStyle = TryResolveActivationCtorOnInvoker (invokerTypeName)?.Style; + } + var peer = new JavaPeerInfo { JavaName = jniName, CompatJniName = compatJniName, @@ -249,6 +266,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A JavaFields = exportFields, ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, + InvokerActivationCtorStyle = invokerActivationCtorStyle, IsGenericDefinition = isGenericDefinition, ComponentAttribute = ToComponentInfo (attrInfo), }; @@ -1280,6 +1298,24 @@ string ManagedTypeToJniDescriptor (string managedType) return null; } + /// + /// Resolve the activation ctor on a known invoker type (search all loaded assemblies). + /// Used for interface peers, whose own type definition has no constructors. + /// The assemblyCache typically contains 10–30 entries (app + framework assemblies), + /// and each lookup is an O(1) dictionary probe, so the linear scan is cheap. + /// + ActivationCtorInfo? TryResolveActivationCtorOnInvoker (string invokerTypeName) + { + foreach (var assembly in assemblyCache.Values) { + if (!assembly.TypesByFullName.TryGetValue (invokerTypeName, out var invokerHandle)) { + continue; + } + var invokerDef = assembly.Reader.GetTypeDefinition (invokerHandle); + return ResolveActivationCtor (invokerTypeName, invokerDef, assembly); + } + return null; + } + public void Dispose () { foreach (var index in assemblyCache.Values) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 2250cb24d67..b4791cfdde0 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -514,9 +514,19 @@ void ProcessContext (HandleContext* context) if (RuntimeFeature.TrimmableTypeMap) { try { + // Map universal request types to concrete peers before proxy lookup. + var resolvedTargetType = ResolvePeerType (targetType); + var typeMap = TrimmableTypeMap.Instance; - var proxy = typeMap.GetProxyForJavaObject (reference.Handle, targetType); - var peer = proxy?.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer); + var proxy = typeMap.GetProxyForJavaObject (reference.Handle, resolvedTargetType); + + // Open-generic proxies cannot instantiate closed targets. + IJavaPeerable? peer; + if (ShouldActivateClosedGenericTarget (proxy, resolvedTargetType)) { + peer = ActivateUsingReflection (resolvedTargetType, reference.Handle, JniHandleOwnership.DoNotTransfer); + } else { + peer = proxy?.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer); + } if (peer is not null) { var peerState = peer.JniManagedPeerState | JniManagedPeerStates.Replaceable; if (global::Java.Interop.Runtime.IsGCUserPeer (peer.PeerReference.Handle)) { @@ -526,7 +536,13 @@ void ProcessContext (HandleContext* context) return peer; } - var targetName = targetType?.AssemblyQualifiedName ?? ""; + // Preserve CreatePeer's bad-cast vs missing-mapping behavior. + if (resolvedTargetType is not null && + IsIncompatibleCast (typeMap, ref reference, resolvedTargetType)) { + return null; + } + + var targetName = resolvedTargetType?.AssemblyQualifiedName ?? ""; var javaType = JniEnvironment.Types.GetJniTypeNameFromInstance (reference); throw new NotSupportedException ( @@ -541,6 +557,85 @@ void ProcessContext (HandleContext* context) return base.CreatePeer (ref reference, transfer, targetType); } + [return: DynamicallyAccessedMembers (Constructors)] + static Type? ResolvePeerType ([DynamicallyAccessedMembers (Constructors)] Type? type) + { + if (type is null) { + return null; + } + if (type == typeof (object) || type == typeof (IJavaPeerable)) { + return typeof (global::Java.Interop.JavaObject); + } + if (type == typeof (Exception)) { + return typeof (JavaException); + } + return type; + } + + static bool ShouldActivateClosedGenericTarget ( + [NotNullWhen (true)] JavaPeerProxy? proxy, + [NotNullWhen (true)] Type? resolvedTargetType) + { + return proxy is not null && + proxy.TargetType.IsGenericTypeDefinition && + resolvedTargetType is not null && + resolvedTargetType.IsGenericType && + !resolvedTargetType.IsGenericTypeDefinition; + } + + static IJavaPeerable? ActivateUsingReflection ( + [DynamicallyAccessedMembers (Constructors)] + Type closedType, + IntPtr handle, + JniHandleOwnership transfer) + { + var ctor = closedType.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); + if (ctor is null) { + return null; + } + + return (IJavaPeerable) ctor.Invoke ([handle, transfer]); + } + + /// + /// Returns true when 's Java class is not assignable from + /// . Throws when has no usable mapping. + /// + static bool IsIncompatibleCast ( + TrimmableTypeMap typeMap, + ref JniObjectReference reference, + Type targetType) + { + if (!typeMap.TryGetJniNameForManagedType (targetType, out var targetJniName)) { + throw new ArgumentException ( + $"Could not determine Java type corresponding to '{targetType.AssemblyQualifiedName}'.", + nameof (targetType)); + } + + var instanceClass = JniEnvironment.Types.GetObjectClass (reference); + JniObjectReference targetClass = default; + try { + try { + targetClass = JniEnvironment.Types.FindClass (targetJniName); + } catch (Java.Lang.ClassNotFoundException e) { + throw new ArgumentException ( + $"Could not find Java class '{targetJniName}'.", + nameof (targetType), e); + } + + if (!JniEnvironment.Types.IsAssignableFrom (instanceClass, targetClass)) { + // Bad cast: callers translate null to the expected result. + return true; + } + } finally { + JniObjectReference.Dispose (ref instanceClass); + JniObjectReference.Dispose (ref targetClass); + } + + // Compatible classes mean a proxy/activation gap. + return false; + } + protected override bool TryConstructPeer ( IJavaPeerable self, ref JniObjectReference reference, diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index eefb7cc2ac5..4f7dbb27c69 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -238,7 +238,16 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) var targetClass = default (JniObjectReference); try { objClass = JniEnvironment.Types.GetObjectClass (selfRef); - targetClass = JniEnvironment.Types.FindClass (targetJniName); + try { + targetClass = JniEnvironment.Types.FindClass (targetJniName); + } catch (Java.Lang.ClassNotFoundException) { + // FindClass throws for managed types whose Java peer class is + // not present in the APK (e.g. test types annotated with + // [JniTypeSignature("__missing__")]). Treat as "no match" so + // JavaMarshalValueManager.CreatePeer can surface the correct + // ArgumentException instead of leaking ClassNotFoundException. + return null; + } var isAssignable = JniEnvironment.Types.IsAssignableFrom (objClass, targetClass); return isAssignable ? proxy : null; } finally { @@ -273,22 +282,26 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) /// internal static bool TargetTypeMatches (Type targetType, Type proxyTargetType) { - if (targetType.IsAssignableFrom (proxyTargetType)) { + if (targetType == proxyTargetType) { return true; } - if (!proxyTargetType.IsGenericTypeDefinition) { - return false; - } - - for (Type? t = targetType; t is not null; t = t.BaseType) { - if (t.IsGenericType && !t.IsGenericTypeDefinition && - t.GetGenericTypeDefinition () == proxyTargetType) { - return true; + // Open generic proxy: match only when targetType is a closed instantiation + // of this generic (e.g. JavaList matches the JavaList<> proxy). + // IsAssignableFrom alone would incorrectly match unrelated open generics + // that are technically subclasses (e.g. JavaArray<> is assignable to + // JavaObject), and proxy.CreateInstance for an open generic always throws. + if (proxyTargetType.IsGenericTypeDefinition) { + for (Type? t = targetType; t is not null; t = t.BaseType) { + if (t.IsGenericType && !t.IsGenericTypeDefinition && + t.GetGenericTypeDefinition () == proxyTargetType) { + return true; + } } + return false; } - return false; + return targetType.IsAssignableFrom (proxyTargetType); } /// diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 6994ce45ade..404d997345d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -471,6 +471,36 @@ sig.ParameterTypes [0].Contains ("JniObjectReference")) { Assert.True (foundByRefCtor, "Expected to find a .ctor with byref JniObjectReference parameter"); } + [Fact] + public void Generate_JiStyleInvoker_FirstParamIsByRef () + { + var peer = MakeInterfacePeer ("test/IJiInvoker", "Test.IJiInvoker", "TestAsm", "Test.IJiInvokerInvoker") with { + InvokerActivationCtorStyle = ActivationCtorStyle.JavaInterop, + }; + + using var stream = GenerateAssembly (new [] { peer }, "JiInvokerByRefTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var ctorRefs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (i => reader.GetMemberReference (MetadataTokens.MemberReferenceHandle (i))) + .Where (m => reader.GetString (m.Name) == ".ctor") + .ToList (); + + bool foundByRefCtor = false; + foreach (var ctor in ctorRefs) { + var sig = ctor.DecodeMethodSignature (SignatureTypeProvider.Instance, null); + if (sig.ParameterTypes.Length == 2 && + sig.ParameterTypes [0].Contains ("JniObjectReference")) { + Assert.True (sig.ParameterTypes [0].EndsWith ("&"), + $"JI-style invoker .ctor first param must be byref, got: {sig.ParameterTypes [0]}"); + foundByRefCtor = true; + } + } + + Assert.True (foundByRefCtor, "Expected to find a JI-style invoker .ctor with byref JniObjectReference parameter"); + } + [Fact] public void Generate_JiStyleCtor_EmitsDeleteRefCall () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs index ec8ba3e7ed0..659de452634 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.Behavior.cs @@ -44,13 +44,14 @@ public void Scan_MarshalMethod_ConstructorsAndSpecialCases () Assert.NotNull (onStart); Assert.Equal ("", onStart.Connector); - var onClick = FindFixtureByManagedName ("Android.Views.IOnClickListener") - .MarshalMethods.FirstOrDefault (m => m.JniName == "onClick"); + var listener = FindFixtureByManagedName ("Android.Views.IOnClickListener"); + var onClick = listener.MarshalMethods.FirstOrDefault (m => m.JniName == "onClick"); Assert.NotNull (onClick); Assert.Equal ("(Landroid/view/View;)V", onClick.JniSignature); - Assert.Equal ("Android.Views.IOnClickListenerInvoker", - FindFixtureByManagedName ("Android.Views.IOnClickListener").InvokerTypeName); + Assert.Equal ("Android.Views.IOnClickListenerInvoker", listener.InvokerTypeName); + Assert.Null (listener.ActivationCtor); + Assert.Equal (ActivationCtorStyle.XamarinAndroid, listener.InvokerActivationCtorStyle); } [Theory] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index 7d57a37657c..fddfaae55a9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -131,4 +131,20 @@ public void Scan_JniTypeSignature_SubclassExtendsJavaPeer () var peer = FindFixtureByJavaName ("net/dot/jni/test/JavaDisposedObject"); Assert.NotNull (peer); } + + [Fact] + public void Scan_JniTypeSignature_ArrayRank_IsExcluded () + { + // Types with [JniTypeSignature(ArrayRank > 0)] represent JNI array wrappers + // (e.g., JavaBooleanArray with IsKeyword=true, or JavaObjectArray without). + // The scanner must skip all of them — they are handled by the built-in tables + // in JniRuntime.JniTypeManager, not the typemap. + var peers = ScanFixtures (); + + // Keyword primitive array (e.g., JavaBooleanArray with "Z") + Assert.DoesNotContain (peers, p => p.ManagedTypeName == "Java.Interop.TestTypes.KeywordPrimitiveArray"); + + // Non-keyword array (e.g., JavaObjectArray with "java/lang/Object", ArrayRank=1) + Assert.DoesNotContain (peers, p => p.ManagedTypeName == "Java.Interop.TestTypes.NonKeywordArrayType"); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 9b220bb6d03..a4363fe9877 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -969,4 +969,23 @@ public class NonGeneratedJavaObject : JavaObject { public NonGeneratedJavaObject () { } } + + /// + /// Mimics Java.Interop.JavaBooleanArray — a primitive array type with IsKeyword=true. + /// The scanner must skip all ArrayRank > 0 types because they are handled by the + /// built-in tables in JniRuntime.JniTypeManager. + /// + [Java.Interop.JniTypeSignature ("Z", IsKeyword = true, ArrayRank = 1, GenerateJavaPeer = false)] + public sealed class KeywordPrimitiveArray : JavaObject + { + } + + /// + /// Mimics Java.Interop.JavaObjectArray — a non-keyword array type with ArrayRank=1. + /// The scanner must also skip these to avoid adding unnecessary aliases. + /// + [Java.Interop.JniTypeSignature ("java/lang/Object", ArrayRank = 1, GenerateJavaPeer = false)] + public class NonKeywordArrayType : JavaObject + { + } } diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets index 2854ac5b224..b1fa7783883 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets @@ -10,7 +10,40 @@ - + + + + + + + + + + managedReferences = new ArrayList(); + + public GetThis () { + } + + public final GetThis getThis () { + return this; + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaObjectExtensionsTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaObjectExtensionsTests.cs index 9938951cae9..1b13316cc3d 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaObjectExtensionsTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaObjectExtensionsTests.cs @@ -15,7 +15,6 @@ namespace Java.InteropTests { [TestFixture] public class JavaObjectExtensionsTests { - // TODO: https://github.com/dotnet/android/issues/11170 — cannot create instance of open generic type under trimmable typemap [Test] public void JavaCast_BaseToGenericWrapper () { @@ -42,7 +41,6 @@ public void JavaCast_InterfaceCast () } } - // TODO: https://github.com/dotnet/android/issues/11170 — throws NotSupportedException instead of InvalidCastException under trimmable typemap [Test] public void JavaCast_BadInterfaceCast () { @@ -69,7 +67,6 @@ public void JavaCast_ObtainOriginalInstance () Assert.AreSame (list, al); } - // TODO: https://github.com/dotnet/android/issues/11170 — throws NotSupportedException instead of InvalidCastException under trimmable typemap [Test] public void JavaCast_InvalidTypeCastThrows () { @@ -78,7 +75,6 @@ public void JavaCast_InvalidTypeCastThrows () } } - // TODO: https://github.com/dotnet/android/issues/11170 — throws NotSupportedException instead of InvalidCastException under trimmable typemap [Test] public void JavaCast_CheckForManagedSubclasses () { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs index d24d53ad324..f3e73568eed 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs @@ -34,10 +34,17 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) // net.dot.jni.test.CallVirtualFromConstructorDerived Java class not in APK "Java.InteropTests.InvokeVirtualFromConstructorTests", - // net.dot.jni.internal.JavaProxyObject Java class not in APK — fixture setup fails (16 tests) + // net.dot.jni.internal.JavaProxyObject. calls + // net.dot.jni.ManagedPeer.registerNativeMembers, which the trimmable + // typemap path rejects (Native methods must be registered by JCW + // static initializer blocks). Fixing this requires a parallel + // Android-trimmable variant of JavaProxyObject.java that registers + // its native equals/hashCode/toString via mono.android.Runtime.register + // — an architectural change tracked separately from the JavaCast / JavaAs + // work in this PR. See https://github.com/dotnet/android/issues/11170. "Java.InteropTests.JavaObjectArray_object_ContractTest", - // net.dot.jni.internal.JavaProxyObject Java class not in APK + // Same root cause as above (JavaProxyObject static init). "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateArgumentState", "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateGenericArgumentState", "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateGenericObjectReferenceArgumentState", @@ -46,18 +53,10 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateValue", "Java.InteropTests.JniValueMarshaler_object_ContractTests.SpecificTypesAreUsed", - // No generated JavaPeerProxy for java/lang/Object with IJavaPeerable target type - "Java.InteropTests.JniValueMarshaler_IJavaPeerable_ContractTests.JniValueMarshalerContractTests`1.CreateGenericValue", - "Java.InteropTests.JniValueMarshaler_IJavaPeerable_ContractTests.JniValueMarshalerContractTests`1.CreateValue", - - // net.dot.jni.internal.JavaProxyThrowable — proxy throwable creation fails + // net.dot.jni.internal.JavaProxyThrowable static init — same JavaProxy* + // root cause as the JavaProxyObject exclusions above. "Java.InteropTests.JavaExceptionTests.InnerExceptionIsNotAProxy", - // IJavaInterfaceInvoker ctor trimmed / missing JavaPeerProxy for test types - "Java.InteropTests.JavaPeerableExtensionsTests.JavaAs", - "Java.InteropTests.JavaPeerableExtensionsTests.JavaAs_Exceptions", - "Java.InteropTests.JavaPeerableExtensionsTests.JavaAs_InstanceThatDoesNotImplementInterfaceReturnsNull", - // JNI method remapping not supported in trimmable typemap "Java.InteropTests.JniPeerMembersTests.ReplaceInstanceMethodName", "Java.InteropTests.JniPeerMembersTests.ReplaceInstanceMethodWithStaticMethod", @@ -67,18 +66,12 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) // net.dot.jni.test.GenericHolder Java class not in APK "Java.InteropTests.JniTypeManagerTests.CannotCreateGenericHolderFromJava", - // JniPrimitiveArrayInfo lookup fails for JavaBooleanArray + // JniPrimitiveArrayInfo lookup fails for JavaBooleanArray — + // our typemap returns JavaBooleanArray for "Z" via JavaPrimitiveArray<> + // alias, which collides with the legacy GetPrimitiveArrayTypesForSimpleReference + // that expects only primitive CLR types. Out of scope for this PR. "Java.InteropTests.JniTypeManagerTests.GetType", - // net.dot.jni.test.GetThis — cannot register native members - "Java.InteropTests.JavaObjectTest.DisposeAccessesThis", - - // NotSupportedException instead of InvalidCastException — no generated JavaPeerProxy - "Java.InteropTests.JavaObjectExtensionsTests.JavaCast_BadInterfaceCast", - "Java.InteropTests.JavaObjectExtensionsTests.JavaCast_BaseToGenericWrapper", - "Java.InteropTests.JavaObjectExtensionsTests.JavaCast_CheckForManagedSubclasses", - "Java.InteropTests.JavaObjectExtensionsTests.JavaCast_InvalidTypeCastThrows", - // Open generic type handling differs from non-trimmable "Java.InteropTests.JnienvTest.NewOpenGenericTypeThrows",