diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 7439f68173a..1baa3d23e92 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -57,8 +57,9 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri ModuleName = moduleName, }; - // Invoker types are NOT emitted as separate proxies or TypeMap entries — - // they only appear as a TypeRef in the interface proxy's get_InvokerType property. + // Invoker types are NOT emitted as separate proxies or TypeMap entries. + // They are associated with their interface/abstract proxy so JniPeerMembers + // can resolve the invoker type's registered JNI name. var invokerTypeNames = new HashSet ( peers.Select (p => p.InvokerTypeName).OfType (), StringComparer.Ordinal); @@ -138,10 +139,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, // Without this, the proxy type map is empty and CreatePeer fails for // interface types like IIterator where targetType-based lookup is needed. if (proxy != null) { - model.Associations.Add (new TypeMapAssociationData { - SourceTypeReference = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName), - AliasProxyTypeReference = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", assemblyName), - }); + AddProxyAssociation (model, peer, proxy, assemblyName); } return; } @@ -174,6 +172,9 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, SourceTypeReference = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName), AliasProxyTypeReference = holderRef, }); + if (proxy != null && peer.InvokerTypeName != null) { + AddProxyAssociation (model, peer.InvokerTypeName, peer.AssemblyName, proxy, assemblyName); + } } // Base JNI name entry → alias holder (self-referencing trim target, kept alive by associations) @@ -198,6 +199,22 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, }); } + static void AddProxyAssociation (TypeMapAssemblyData model, JavaPeerInfo peer, JavaPeerProxyData proxy, string assemblyName) + { + AddProxyAssociation (model, peer.ManagedTypeName, peer.AssemblyName, proxy, assemblyName); + if (peer.InvokerTypeName != null) { + AddProxyAssociation (model, peer.InvokerTypeName, peer.AssemblyName, proxy, assemblyName); + } + } + + static void AddProxyAssociation (TypeMapAssemblyData model, string managedTypeName, string sourceAssemblyName, JavaPeerProxyData proxy, string outputAssemblyName) + { + model.Associations.Add (new TypeMapAssociationData { + SourceTypeReference = AssemblyQualify (managedTypeName, sourceAssemblyName), + AliasProxyTypeReference = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", outputAssemblyName), + }); + } + /// /// Determines whether a type should use the unconditional (2-arg) TypeMap attribute. /// Unconditional types are always preserved by the trimmer. @@ -314,8 +331,8 @@ static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy) WrapperName = $"n_{mm.JniName}_uco_{ucoIndex}", CallbackMethodName = mm.NativeCallbackName, CallbackType = new TypeRefData { - ManagedTypeName = !string.IsNullOrEmpty (mm.DeclaringTypeName) ? mm.DeclaringTypeName : peer.ManagedTypeName, - AssemblyName = !string.IsNullOrEmpty (mm.DeclaringAssemblyName) ? mm.DeclaringAssemblyName : peer.AssemblyName, + ManagedTypeName = !mm.DeclaringTypeName.IsNullOrEmpty () ? mm.DeclaringTypeName : peer.ManagedTypeName, + AssemblyName = !mm.DeclaringAssemblyName.IsNullOrEmpty () ? mm.DeclaringAssemblyName : peer.AssemblyName, }, JniSignature = mm.JniSignature, }); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 07c85992c21..26a75348564 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -16,6 +16,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// sealed class PEAssemblyBuilder { + const int DefaultMaxStack = 32; + // Mono.Android strong name public key token (84e04ff9cfb79065) static readonly byte [] MonoAndroidPublicKeyToken = { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 }; @@ -307,7 +309,7 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, var bodyEncoder = new MethodBodyStreamEncoder (ILBuilder); int bodyOffset = localSigHandle.IsNil ? bodyEncoder.AddMethodBody (encoder) - : bodyEncoder.AddMethodBody (encoder, maxStack: 8, localSigHandle, MethodBodyAttributes.InitLocals); + : bodyEncoder.AddMethodBody (encoder, maxStack: DefaultMaxStack, localSigHandle, MethodBodyAttributes.InitLocals); return Metadata.AddMethodDefinition ( attrs, MethodImplAttributes.IL, @@ -352,7 +354,7 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, var bodyEncoder = new MethodBodyStreamEncoder (ILBuilder); int bodyOffset = localSigHandle.IsNil ? bodyEncoder.AddMethodBody (encoder) - : bodyEncoder.AddMethodBody (encoder, maxStack: 8, localSigHandle, MethodBodyAttributes.InitLocals); + : bodyEncoder.AddMethodBody (encoder, maxStack: DefaultMaxStack, localSigHandle, MethodBodyAttributes.InitLocals); return Metadata.AddMethodDefinition ( attrs, MethodImplAttributes.IL, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index d6b7bae9a22..56eed743c84 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -108,8 +108,13 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1)); } - // Emit [assembly: TypeMapAssemblyTargetAttribute<__TypeMapAnchor>("name")] for each per-assembly typemap - EmitAssemblyTargetAttributes (pe, anchorTypeHandle, perAssemblyTypeMapNames); + // Emit [assembly: TypeMapAssemblyTargetAttribute("name")] for each per-assembly typemap. + // T must match the group type later passed to TypeMapping.GetOrCreate*TypeMapping(). + if (useSharedTypemapUniverse) { + EmitSharedUniverseAssemblyTargetAttributes (pe, anchorTypeHandle, perAssemblyTypeMapNames); + } else { + EmitPerAssemblyUniverseAssemblyTargetAttributes (pe, perAssemblyTypeMapNames); + } // Emit [assembly: IgnoresAccessChecksTo("...")] so TypeMapLoader.Initialize() can access // internal types (SingleUniverseTypeMap, AggregateTypeMap in Mono.Android, @@ -126,23 +131,47 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha pe.WritePE (stream); } - static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames) + static void EmitSharedUniverseAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames) { - var openAttrRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeInteropServicesRef, + var openAttrRef = GetTypeMapAssemblyTargetAttributeRef (pe); + var ctorRef = GetTypeMapAssemblyTargetAttributeCtorRef (pe, openAttrRef, anchorTypeHandle); + foreach (var name in perAssemblyTypeMapNames) { + EmitAssemblyTargetAttribute (pe, ctorRef, name); + } + } + + static void EmitPerAssemblyUniverseAssemblyTargetAttributes (PEAssemblyBuilder pe, IReadOnlyList perAssemblyTypeMapNames) + { + var openAttrRef = GetTypeMapAssemblyTargetAttributeRef (pe); + foreach (var name in perAssemblyTypeMapNames) { + var asmRef = pe.FindOrAddAssemblyRef (name); + var perAssemblyAnchorRef = pe.Metadata.AddTypeReference (asmRef, + default, pe.Metadata.GetOrAddString ("__TypeMapAnchor")); + var ctorRef = GetTypeMapAssemblyTargetAttributeCtorRef (pe, openAttrRef, perAssemblyAnchorRef); + EmitAssemblyTargetAttribute (pe, ctorRef, name); + } + } + + static TypeReferenceHandle GetTypeMapAssemblyTargetAttributeRef (PEAssemblyBuilder pe) + { + return pe.Metadata.AddTypeReference (pe.SystemRuntimeInteropServicesRef, pe.Metadata.GetOrAddString ("System.Runtime.InteropServices"), pe.Metadata.GetOrAddString ("TypeMapAssemblyTargetAttribute`1")); + } + static MemberReferenceHandle GetTypeMapAssemblyTargetAttributeCtorRef (PEAssemblyBuilder pe, EntityHandle openAttrRef, EntityHandle anchorTypeHandle) + { var closedAttrTypeSpec = pe.MakeGenericTypeSpec (openAttrRef, anchorTypeHandle); - - var ctorRef = pe.AddMemberRef (closedAttrTypeSpec, ".ctor", + return pe.AddMemberRef (closedAttrTypeSpec, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, rt => rt.Void (), p => p.AddParameter ().Type ().String ())); + } - foreach (var name in perAssemblyTypeMapNames) { - var blobHandle = pe.BuildAttributeBlob (blob => blob.WriteSerializedString (name)); - pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle); - } + static void EmitAssemblyTargetAttribute (PEAssemblyBuilder pe, MemberReferenceHandle ctorRef, string name) + { + var blobHandle = pe.BuildAttributeBlob (blob => blob.WriteSerializedString (name)); + pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle); } static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index d489edcdc07..ccf1ac857a4 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -38,9 +38,15 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// // JniName / TargetType / InvokerType are supplied by the base JavaPeerProxy constructor. /// /// // UCO wrappers — [UnmanagedCallersOnly] entry points for JNI native methods (ACWs only): -/// [UnmanagedCallersOnly] /// public static void n_OnCreate_uco_0(IntPtr jnienv, IntPtr self, IntPtr p0) -/// => Activity.n_OnCreate(jnienv, self, p0); +/// { +/// AndroidRuntimeInternal.WaitForBridgeProcessing(); +/// try { +/// Activity.n_OnCreate(jnienv, self, p0); +/// } catch (Exception e) { +/// AndroidEnvironmentInternal.UnhandledException(e); +/// } +/// } /// /// [UnmanagedCallersOnly] /// public static void nctor_0_uco(IntPtr jnienv, IntPtr self) @@ -93,6 +99,8 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniObjectReferenceCtorRef; MemberReferenceHandle _jniEnvDeleteRefRef; MemberReferenceHandle _shouldSkipActivationRef; + MemberReferenceHandle _waitForBridgeProcessingRef; + MemberReferenceHandle _androidEnvironmentUnhandledExceptionRef; MemberReferenceHandle _ucoAttrCtorRef; BlobHandle _ucoAttrBlobHandle; MemberReferenceHandle _typeMapAttrCtorRef2Arg; @@ -106,6 +114,8 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _jniTransitionRef; TypeReferenceHandle _jniRuntimeRef; TypeReferenceHandle _exceptionRef; + TypeReferenceHandle _androidRuntimeInternalRef; + TypeReferenceHandle _androidEnvironmentInternalRef; MemberReferenceHandle _beginMarshalMethodRef; MemberReferenceHandle _endMarshalMethodRef; @@ -238,6 +248,11 @@ void EmitTypeReferences () metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniRuntime")); _exceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Exception")); + var monoAndroidRuntimeRef = _pe.AddAssemblyRef ("Mono.Android.Runtime", new Version (0, 0, 0, 0)); + _androidRuntimeInternalRef = metadata.AddTypeReference (monoAndroidRuntimeRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("AndroidRuntimeInternal")); + _androidEnvironmentInternalRef = metadata.AddTypeReference (monoAndroidRuntimeRef, + metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("AndroidEnvironmentInternal")); // ReadOnlySpan — TypeSpec for generic instantiation _readOnlySpanOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, @@ -310,6 +325,14 @@ void EmitMemberReferences () rt => rt.Type ().Boolean (), p => { p.AddParameter ().Type ().IntPtr (); })); + _waitForBridgeProcessingRef = _pe.AddMemberRef (_androidRuntimeInternalRef, "WaitForBridgeProcessing", + sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { })); + + _androidEnvironmentUnhandledExceptionRef = _pe.AddMemberRef (_androidEnvironmentInternalRef, "UnhandledException", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().Type (_exceptionRef, false))); + // JniNativeMethod..ctor(byte*, byte*, IntPtr) _jniNativeMethodCtorRef = _pe.AddMemberRef (_jniNativeMethodRef, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3, @@ -351,7 +374,7 @@ void EmitMemberReferences () _ucoAttrCtorRef = _pe.AddMemberRef (ucoAttrTypeRef, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); - // Pre-compute the UCO attribute blob — it's always the same 4 bytes (prolog + no named args) + // Legacy marshal-method UCO wrappers use the default unmanaged calling convention. _ucoAttrBlobHandle = _pe.BuildAttributeBlob (b => { }); // JniEnvironment.BeginMarshalMethod(nint jnienv, out JniTransition, out JniRuntime?) -> bool @@ -524,7 +547,7 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary { + (encoder, cfb) => EmitUcoForwarderBody (encoder, cfb, returnKind, enc => { for (int p = 0; p < paramCount; p++) - encoder.LoadArgument (p); - encoder.Call (callbackRef); - encoder.OpCode (ILOpCode.Ret); - }); + enc.LoadArgument (p); + enc.Call (callbackRef); + }), + blob => EncodeUcoForwarderLegacyLocals (blob, returnKind)); AddUnmanagedCallersOnlyAttribute (handle); return handle; } + void EmitUcoForwarderBody (InstructionEncoder encoder, ControlFlowBuilder cfb, JniParamKind returnKind, Action emitCallback) + { + bool isVoid = returnKind == JniParamKind.Void; + var tryStart = encoder.DefineLabel (); + var catchStart = encoder.DefineLabel (); + var afterAll = encoder.DefineLabel (); + + encoder.Call (_waitForBridgeProcessingRef); + encoder.MarkLabel (tryStart); + emitCallback (encoder); + if (!isVoid) { + encoder.StoreLocal (0); + } + encoder.Branch (ILOpCode.Leave, afterAll); + + encoder.MarkLabel (catchStart); + encoder.StoreLocal (isVoid ? 0 : 1); + encoder.LoadLocal (isVoid ? 0 : 1); + encoder.Call (_androidEnvironmentUnhandledExceptionRef); + encoder.Branch (ILOpCode.Leave, afterAll); + + encoder.MarkLabel (afterAll); + if (!isVoid) { + encoder.LoadLocal (0); + } + encoder.OpCode (ILOpCode.Ret); + + cfb.AddCatchRegion (tryStart, catchStart, catchStart, afterAll, _exceptionRef); + } + + void EncodeUcoForwarderLegacyLocals (BlobBuilder blob, JniParamKind returnKind) + { + bool isVoid = returnKind == JniParamKind.Void; + blob.WriteByte (0x07); // LOCAL_SIG + blob.WriteCompressedInteger (isVoid ? 1 : 2); + if (!isVoid) { + JniSignatureHelper.EncodeClrType (new SignatureTypeEncoder (blob), returnKind); + } + blob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_exceptionRef)); + } + MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxyData proxy) { var targetTypeRef = _pe.ResolveTypeRef (uco.TargetType); @@ -1115,10 +1180,11 @@ void EncodeUcoConstructorLocals_JavaInterop (BlobBuilder blob) blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniObjectReferenceRef)); } - void EmitRegisterNatives (List registrations, + void EmitRegisterNatives (JavaPeerProxyData proxy, Dictionary wrapperHandles) { // Filter to only registrations that have corresponding wrapper methods + var registrations = proxy.NativeRegistrations; var validRegs = new List<(NativeRegistrationData Reg, MethodDefinitionHandle Wrapper)> (registrations.Count); foreach (var reg in registrations) { if (wrapperHandles.TryGetValue (reg.WrapperMethodName, out var wrapperHandle)) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 9bf542f6b7b..bfd2db7feac 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -103,10 +103,14 @@ void Build () attrInfo ??= CreateTypeAttributeInfo (attrName); var value = DecodeAttribute (ca); + if (attrName == "ContentProviderAttribute") { + AddContentProviderAuthorities (value, attrInfo.Properties); + } + // Capture all named properties foreach (var named in value.NamedArguments) { if (named.Name is not null) { - attrInfo.Properties [named.Name] = named.Value; + attrInfo.Properties [named.Name] = GetComponentPropertyValue (named.Name, named.Value); } } @@ -355,6 +359,42 @@ IntentFilterInfo ParseIntentFilterAttribute (CustomAttribute ca) }; } + static void AddContentProviderAuthorities (CustomAttributeValue value, Dictionary properties) + { + if (value.FixedArguments.Length == 0) { + return; + } + + if (TryGetStringArray (value.FixedArguments [0].Value, out var authorities)) { + properties ["Authorities"] = string.Join (";", authorities); + } + } + + static object? GetComponentPropertyValue (string name, object? value) + { + if (name == "Authorities" && TryGetStringArray (value, out var authorities)) { + return string.Join (";", authorities); + } + + return value; + } + + static bool TryGetStringArray (object? value, [NotNullWhen (true)] out List? strings) + { + if (value is IReadOnlyCollection> args) { + strings = new List (args.Count); + foreach (var arg in args) { + if (arg.Value is string s) { + strings.Add (s); + } + } + return true; + } + + strings = null; + return false; + } + static bool TryGetNamedArgument (CustomAttributeValue value, string argumentName, [MaybeNullWhen (false)] out T argumentValue) where T : notnull { foreach (var named in value.NamedArguments) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 8b32abf3940..dda7460271c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -1283,10 +1283,10 @@ string ManagedTypeToJniDescriptor (string managedType) // We want just the type name (before the first comma, if any) var commaIndex = connector.IndexOf (','); if (commaIndex > 0) { - return connector.Substring (0, commaIndex).Trim (); + return NormalizeConnectorManagedTypeName (connector.Substring (0, commaIndex)); } if (connector.Length > 0) { - return connector; + return NormalizeConnectorManagedTypeName (connector); } } @@ -1298,6 +1298,11 @@ string ManagedTypeToJniDescriptor (string managedType) return null; } + static string NormalizeConnectorManagedTypeName (string managedTypeName) + { + return managedTypeName.Trim ().Replace ('/', '+'); + } + /// /// Resolve the activation ctor on a known invoker type (search all loaded assemblies). /// Used for interface peers, whose own type definition has no constructors. @@ -1496,11 +1501,11 @@ static void ParseConnectorDeclaringType (string? connector, out string declaring if (commaIndex < 0) { // No assembly information; treat the whole segment as the type name - declaringTypeName = typeQualified.Trim ().Replace ('/', '+'); + declaringTypeName = NormalizeConnectorManagedTypeName (typeQualified); return; } - declaringTypeName = typeQualified.Substring (0, commaIndex).Trim ().Replace ('/', '+'); + declaringTypeName = NormalizeConnectorManagedTypeName (typeQualified.Substring (0, commaIndex)); string rest = typeQualified.Substring (commaIndex + 1).Trim (); int nextComma = rest.IndexOf (','); declaringAssemblyName = nextComma >= 0 ? rest.Substring (0, nextComma).Trim () : rest.Trim (); diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index f59a8568f66..2e8898e4616 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -134,6 +134,10 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) java_class_loader = args->grefLoader; BoundExceptionType = (BoundExceptionType)args->ioExceptionType; + if (RuntimeFeature.TrimmableTypeMap) { + InitializeTrimmableTypeMapData (); + } + JniRuntime.JniTypeManager typeManager; JniRuntime.JniValueManager? valueManager = null; if (RuntimeFeature.TrimmableTypeMap) { @@ -161,6 +165,11 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) args->jniAddNativeMethodRegistrationAttributePresent != 0 ); JniRuntime.SetCurrent (androidRuntime); + if (RuntimeFeature.TrimmableTypeMap) { + // TypeMapLoader.Initialize() only loads managed typemap data. Registering + // mono.android.Runtime natives requires JniRuntime.Current and its ClassLoader. + TrimmableTypeMap.RegisterNativeMethods (); + } grefIGCUserPeer_class = args->grefIGCUserPeer; grefGCUserPeerable_class = args->grefGCUserPeerable; @@ -179,9 +188,6 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) if (!RuntimeFeature.TrimmableTypeMap) { args->registerJniNativesFn = (IntPtr)(delegate* unmanaged)&RegisterJniNatives; } - if (RuntimeFeature.TrimmableTypeMap) { - InitializeTrimmableTypeMap (); - } RunStartupHooksIfNeeded (); SetSynchronizationContext (); } @@ -193,7 +199,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) // Separate method so the JIT doesn't try to resolve TypeMapLoader (from _Microsoft.Android.TypeMaps.dll) // when compiling JNIEnvInit.Initialize() in non-trimmable builds where that assembly isn't present. [MethodImpl (MethodImplOptions.NoInlining)] - static void InitializeTrimmableTypeMap () + static void InitializeTrimmableTypeMapData () { TypeMapLoader.Initialize (); } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs index 0849741e96d..0c706554fac 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs @@ -15,9 +15,7 @@ namespace Microsoft.Android.Runtime; interface ITypeMapWithAliasing { /// - /// Returns all types mapped to a JNI name, resolving alias holders. - /// For non-alias entries this yields a single type. For alias groups - /// it follows each alias key and yields the surviving target types. + /// Returns all proxy types mapped to a JNI name, resolving alias holders. /// IEnumerable GetTypes (string jniName); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 4f7dbb27c69..1a08fdca094 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -22,6 +22,7 @@ public class TrimmableTypeMap static readonly Lock s_initLock = new (); static readonly JavaPeerProxy s_noPeerSentinel = new MissingJavaPeerProxy (); static TrimmableTypeMap? s_instance; + static bool s_nativeMethodsRegistered; internal static TrimmableTypeMap Instance => s_instance ?? throw new InvalidOperationException ( @@ -77,28 +78,29 @@ static void InitializeCore (ITypeMapWithAliasing typeMap) throw new InvalidOperationException ("TrimmableTypeMap has already been initialized."); } - var instance = new TrimmableTypeMap (typeMap); - instance.RegisterNatives (); - s_instance = instance; + s_instance = new TrimmableTypeMap (typeMap); } } - unsafe void RegisterNatives () + internal static unsafe void RegisterNativeMethods () { - // Use the `string` overload of `JniType` deliberately. Its underlying - // `JniEnvironment.Types.TryFindClass(string, bool)` tries raw JNI `FindClass` - // first and, if that fails, falls back to `Class.forName(name, true, info.Runtime.ClassLoader)`, - // which resolves via the runtime's app ClassLoader — the same one that loads - // `mono.android.Runtime` from the APK. - // The `ReadOnlySpan` overload (see external/Java.Interop/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs) - // only calls raw JNI `FindClass`, which resolves via the system ClassLoader on - // Android and returns a different `Class` instance from the one JCWs reference. - // Registering natives on that other instance is silently wrong. - using var runtimeClass = new JniType ("mono/android/Runtime"); - fixed (byte* name = "registerNatives"u8, sig = "(Ljava/lang/Class;)V"u8) { - var onRegisterNatives = (IntPtr)(delegate* unmanaged)&OnRegisterNatives; - var method = new JniNativeMethod (name, sig, onRegisterNatives); - JniEnvironment.Types.RegisterNatives (runtimeClass.PeerReference, [method]); + lock (s_initLock) { + if (s_nativeMethodsRegistered) { + throw new InvalidOperationException ("TrimmableTypeMap native methods have already been registered."); + } + + if (s_instance is null) { + throw new InvalidOperationException ( + "TrimmableTypeMap has not been initialized. Ensure RuntimeFeature.TrimmableTypeMap is enabled and the JNI runtime is initialized."); + } + + using var runtimeClass = new JniType ("mono/android/Runtime"u8); + fixed (byte* name = "registerNatives"u8, sig = "(Ljava/lang/Class;)V"u8) { + var onRegisterNatives = (IntPtr)(delegate* unmanaged)&OnRegisterNatives; + var method = new JniNativeMethod (name, sig, onRegisterNatives); + JniEnvironment.Types.RegisterNatives (runtimeClass.PeerReference, [method]); + } + s_nativeMethodsRegistered = true; } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index ffa3d509f8b..2253be9a160 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -15,6 +16,9 @@ namespace Microsoft.Android.Runtime; /// class TrimmableTypeMapTypeManager : JniRuntime.JniTypeManager { + const string NoSimpleReference = "\0"; + readonly ConcurrentDictionary _simpleReferenceCache = new (); + protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference)) { @@ -28,6 +32,33 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl } } + protected override string? GetSimpleReference (Type type) + { + var simpleReference = _simpleReferenceCache.GetOrAdd (type, GetSimpleReferenceUncached); + return simpleReference == NoSimpleReference ? null : simpleReference; + } + + string GetSimpleReferenceUncached (Type type) + { + if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName)) { + return jniName; + } + + foreach (var r in base.GetSimpleReferences (type)) { + return r; + } + + // Walk the base type chain for managed-only subclasses (e.g., JavaProxyThrowable + // extends Java.Lang.Error but has no [Register] attribute itself). + for (var baseType = type.BaseType; baseType is not null; baseType = baseType.BaseType) { + if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (baseType, out var baseJniName)) { + return baseJniName; + } + } + + return NoSimpleReference; + } + protected override IEnumerable GetSimpleReferences (Type type) { if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName)) { diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index d30ab44e74b..d0ca9742e30 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -30,42 +30,61 @@ This target batches over @(_BuildTargetAbis) to add items per-ABI. --> - - <_CurrentAbi>%(_BuildTargetAbis.Identity) - <_CurrentRid Condition=" '$(_CurrentAbi)' == 'arm64-v8a' ">android-arm64 - <_CurrentRid Condition=" '$(_CurrentAbi)' == 'armeabi-v7a' ">android-arm - <_CurrentRid Condition=" '$(_CurrentAbi)' == 'x86_64' ">android-x64 - <_CurrentRid Condition=" '$(_CurrentAbi)' == 'x86' ">android-x86 - + DependsOnTargets="_PrepareTrimmableTypeMapAssemblies;_DefineBuildTargetAbis"> + + <_TrimmableTypeMapAbi Include="@(_BuildTargetAbis)" /> + <_TrimmableTypeMapAbi Update="@(_TrimmableTypeMapAbi)" Condition=" '%(_TrimmableTypeMapAbi.Identity)' == 'arm64-v8a' "> + android-arm64 + + <_TrimmableTypeMapAbi Update="@(_TrimmableTypeMapAbi)" Condition=" '%(_TrimmableTypeMapAbi.Identity)' == 'armeabi-v7a' "> + android-arm + + <_TrimmableTypeMapAbi Update="@(_TrimmableTypeMapAbi)" Condition=" '%(_TrimmableTypeMapAbi.Identity)' == 'x86_64' "> + android-x64 + + <_TrimmableTypeMapAbi Update="@(_TrimmableTypeMapAbi)" Condition=" '%(_TrimmableTypeMapAbi.Identity)' == 'x86' "> + android-x86 + + + - <_CurrentLinkedTypeMapDlls Include="$(IntermediateOutputPath)$(_CurrentRid)/linked/_*.TypeMap.dll;$(IntermediateOutputPath)$(_CurrentRid)/linked/_Microsoft.Android.TypeMap*.dll" /> + <_LinkedTypeMapDlls Include="$(IntermediateOutputPath)%(_TrimmableTypeMapAbi.RuntimeIdentifier)/linked/_*.TypeMap.dll;$(IntermediateOutputPath)%(_TrimmableTypeMapAbi.RuntimeIdentifier)/linked/_Microsoft.Android.TypeMap*.dll"> + %(_TrimmableTypeMapAbi.Identity) + %(_TrimmableTypeMapAbi.RuntimeIdentifier) + %(_TrimmableTypeMapAbi.Identity)/%(_LinkedTypeMapDlls.Filename)%(_LinkedTypeMapDlls.Extension) + %(_TrimmableTypeMapAbi.Identity)/ + - - <_BuildApkResolvedUserAssemblies Include="@(_CurrentLinkedTypeMapDlls)"> - $(_CurrentAbi) - $(_CurrentRid) - $(_CurrentAbi)/%(_CurrentLinkedTypeMapDlls.Filename)%(_CurrentLinkedTypeMapDlls.Extension) - $(_CurrentAbi)/ + + <_BuildApkResolvedUserAssemblies Include="@(_LinkedTypeMapDlls)"> + %(_LinkedTypeMapDlls.Abi) + %(_LinkedTypeMapDlls.RuntimeIdentifier) + %(_LinkedTypeMapDlls.DestinationSubPath) + %(_LinkedTypeMapDlls.DestinationSubDirectory) - - <_CurrentTypeMapDlls Include="$(_TypeMapOutputDirectory)*.dll" /> + + <_TypeMapDlls Include="$(_TypeMapOutputDirectory)*.dll"> + %(_TrimmableTypeMapAbi.Identity) + %(_TrimmableTypeMapAbi.RuntimeIdentifier) + %(_TrimmableTypeMapAbi.Identity)/%(_TypeMapDlls.Filename)%(_TypeMapDlls.Extension) + %(_TrimmableTypeMapAbi.Identity)/ + - - <_BuildApkResolvedUserAssemblies Include="@(_CurrentTypeMapDlls)"> - $(_CurrentAbi) - $(_CurrentRid) - $(_CurrentAbi)/%(_CurrentTypeMapDlls.Filename)%(_CurrentTypeMapDlls.Extension) - $(_CurrentAbi)/ + + <_BuildApkResolvedUserAssemblies Include="@(_TypeMapDlls)"> + %(_TypeMapDlls.Abi) + %(_TypeMapDlls.RuntimeIdentifier) + %(_TypeMapDlls.DestinationSubPath) + %(_TypeMapDlls.DestinationSubDirectory) - <_CurrentLinkedTypeMapDlls Remove="@(_CurrentLinkedTypeMapDlls)" /> - <_CurrentTypeMapDlls Remove="@(_CurrentTypeMapDlls)" /> + <_LinkedTypeMapDlls Remove="@(_LinkedTypeMapDlls)" /> + <_TypeMapDlls Remove="@(_TypeMapDlls)" /> + <_TrimmableTypeMapAbi Remove="@(_TrimmableTypeMapAbi)" /> diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 123254579b5..6ff49d6868c 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -111,6 +111,45 @@ + + + + <_TypeMapFirstAbi Condition=" '$(AndroidSupportedAbis)' != '' ">$([System.String]::Copy('$(AndroidSupportedAbis)').Split(';')[0]) + <_TypeMapFirstAbi Condition=" '$(_TypeMapFirstAbi)' == '' ">arm64-v8a + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'arm64-v8a' ">android-arm64 + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'armeabi-v7a' ">android-arm + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'x86_64' ">android-x64 + <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'x86' ">android-x86 + + + + + <_ResolvedAssemblies Include="$(_TypeMapOutputDirectory)*.dll"> + $(_TypeMapFirstAbi) + $(_TypeMapFirstRid) + $(_TypeMapFirstAbi)/%(Filename)%(Extension) + $(_TypeMapFirstAbi)/ + + <_ShrunkAssemblies Include="$(_TypeMapOutputDirectory)*.dll"> + $(_TypeMapFirstAbi) + $(_TypeMapFirstRid) + $(_TypeMapFirstAbi)/%(Filename)%(Extension) + $(_TypeMapFirstAbi)/ + + + + - - <_TypeMapFirstAbi Condition=" '$(AndroidSupportedAbis)' != '' ">$([System.String]::Copy('$(AndroidSupportedAbis)').Split(';')[0]) - <_TypeMapFirstAbi Condition=" '$(_TypeMapFirstAbi)' == '' ">arm64-v8a - <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'arm64-v8a' ">android-arm64 - <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'armeabi-v7a' ">android-arm - <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'x86_64' ">android-x64 - <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'x86' ">android-x86 - - - - - <_ResolvedAssemblies Include="$(_TypeMapOutputDirectory)*.dll"> - $(_TypeMapFirstAbi) - $(_TypeMapFirstRid) - $(_TypeMapFirstAbi)/%(Filename)%(Extension) - $(_TypeMapFirstAbi)/ - - <_ShrunkAssemblies Include="$(_TypeMapOutputDirectory)*.dll"> - $(_TypeMapFirstAbi) - $(_TypeMapFirstRid) - $(_TypeMapFirstAbi)/%(Filename)%(Extension) - $(_TypeMapFirstAbi)/ - - - > jav // Managed types, however, must point back to the original Java type instead // File/assembly generator use the `DuplicateForJavaToManaged` field to know to which managed type the // duplicate Java type must be mapped. - TypeMapDebugEntry template = duplicates [0]; - for (int i = 1; i < duplicates.Count; i++) { - duplicates [i].DuplicateForJavaToManaged = template; + TypeMapDebugEntry template = GetDebugDuplicateTemplate (duplicates); + foreach (TypeMapDebugEntry duplicate in duplicates) { + if (duplicate == template) { + continue; + } + duplicate.DuplicateForJavaToManaged = template; } } } + static TypeMapDebugEntry GetDebugDuplicateTemplate (List duplicates) + { + foreach (TypeMapDebugEntry duplicate in duplicates) { + if (duplicate.AssemblyName == "Mono.Android") { + return duplicate; + } + } + return duplicates [0]; + } + static void UpdateApplicationConfig (NativeCodeGenState state, TypeDefinition javaType) { state.JniAddNativeMethodRegistrationAttributePresent = JniAddNativeMethodRegistrationAttributeFound (state.JniAddNativeMethodRegistrationAttributePresent, javaType); diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs index 0bd20902776..4ef17765d10 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/TypeMappingDebugNativeAssemblyGeneratorCLR.cs @@ -305,12 +305,12 @@ protected override void Construct (LlvmIrModule module) // Java-to-managed maps don't use hashes since many mappings have multiple instances foreach (TypeMapGenerator.TypeMapDebugEntry entry in data.JavaToManagedMap) { TypeMapGenerator.TypeMapDebugEntry managedEntry = entry.DuplicateForJavaToManaged != null ? entry.DuplicateForJavaToManaged : entry; - (int managedTypeNameOffset, int _) = managedTypeNames.Add (entry.ManagedName); + (int managedTypeNameOffset, int _) = managedTypeNames.Add (managedEntry.ManagedName); (int javaTypeNameOffset, int _) = javaTypeNames.Add (entry.JavaName); var j2m = new TypeMapEntry { From = entry.JavaName, - To = managedEntry.SkipInJavaToManaged ? String.Empty : entry.ManagedName, + To = managedEntry.SkipInJavaToManaged ? String.Empty : managedEntry.ManagedName, from = (uint)javaTypeNameOffset, from_hash = 0, @@ -318,17 +318,17 @@ protected override void Construct (LlvmIrModule module) }; javaToManagedMap.Add (new StructureInstance (typeMapEntryStructureInfo, j2m)); - int assemblyNameOffset = assemblyNamesBlob.GetIndexOf (entry.AssemblyName); + int assemblyNameOffset = assemblyNamesBlob.GetIndexOf (managedEntry.AssemblyName); if (assemblyNameOffset < 0) { - throw new InvalidOperationException ($"Internal error: assembly name '{entry.AssemblyName}' not found in the assembly names blob."); + throw new InvalidOperationException ($"Internal error: assembly name '{managedEntry.AssemblyName}' not found in the assembly names blob."); } var typeInfo = new TypeMapManagedTypeInfo { - AssemblyName = entry.AssemblyName, - ManagedTypeName = entry.ManagedName, + AssemblyName = managedEntry.AssemblyName, + ManagedTypeName = managedEntry.ManagedName, assembly_name_index = (uint)assemblyNameOffset, - managed_type_token_id = entry.ManagedTypeTokenId, + managed_type_token_id = managedEntry.ManagedTypeTokenId, }; managedTypeInfos.Add (new StructureInstance (typeMapManagedTypeInfoStructureInfo, typeInfo)); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index 98b28be0591..d9bb30c1ecf 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -143,22 +143,4 @@ private protected static List GetMemberRefNames (MetadataReader reader) .Select (m => reader.GetString (m.Name)) .ToList (); - /// - /// Returns true if the IL byte stream contains a Call (0x28) or Callvirt (0x6F) instruction - /// whose metadata token matches . - /// - private protected static bool ILContainsCallToken (byte[] ilBytes, int token) - { - byte t0 = (byte)(token & 0xFF); - byte t1 = (byte)((token >> 8) & 0xFF); - byte t2 = (byte)((token >> 16) & 0xFF); - byte t3 = (byte)((token >> 24) & 0xFF); - for (int i = 0; i < ilBytes.Length - 4; i++) { - if ((ilBytes[i] == 0x28 || ilBytes[i] == 0x6F) && - ilBytes[i + 1] == t0 && ilBytes[i + 2] == t1 && - ilBytes[i + 3] == t2 && ilBytes[i + 4] == t3) - return true; - } - return false; - } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 84dd6a0a2f4..1a14dd1c09c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; using Xunit; @@ -93,25 +94,47 @@ public void Generate_AttributeBlobValues_MatchTargetNames () using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); - var targetAttrs = GetTypeMapAssemblyTargetAttributes (reader); - - var attrValues = new List (); - foreach (var attr in targetAttrs) { - var blob = reader.GetBlobReader (attr.Value); - - // Custom attribute blob: prolog (2 bytes) + SerString value - var prolog = blob.ReadUInt16 (); - Assert.Equal (1, prolog); // ECMA-335 prolog - var value = blob.ReadSerializedString (); - Assert.NotNull (value); - attrValues.Add (value!); - } + var attrValues = GetTypeMapAssemblyTargetAttributeTargets (reader) + .Select (target => target.TargetName) + .ToList (); Assert.Equal (2, attrValues.Count); Assert.Contains ("_App.TypeMap", attrValues); Assert.Contains ("_Mono.Android.TypeMap", attrValues); } + [Fact] + public void Generate_AggregateMode_TargetAttributesUsePerAssemblyAnchors () + { + var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }; + using var stream = GenerateRootAssembly (targets, useSharedTypemapUniverse: false); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var targetAttributes = GetTypeMapAssemblyTargetAttributeTargets (reader); + + Assert.Equal (new [] { + ("_App.TypeMap", "_App.TypeMap"), + ("_Mono.Android.TypeMap", "_Mono.Android.TypeMap"), + }, targetAttributes); + } + + [Fact] + public void Generate_MergedMode_TargetAttributesUseSharedAnchor () + { + var targets = new [] { "_App.TypeMap", "_Mono.Android.TypeMap" }; + using var stream = GenerateRootAssembly (targets, useSharedTypemapUniverse: true); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var targetAttributes = GetTypeMapAssemblyTargetAttributeTargets (reader); + + Assert.Equal (new [] { + ("_App.TypeMap", "Mono.Android"), + ("_Mono.Android.TypeMap", "Mono.Android"), + }, targetAttributes); + } + static List GetTypeMapAssemblyTargetAttributes (MetadataReader reader) { var result = new List (); @@ -128,6 +151,66 @@ static List GetTypeMapAssemblyTargetAttributes (MetadataReader return result; } + static List<(string TargetName, string GenericArgumentScope)> GetTypeMapAssemblyTargetAttributeTargets (MetadataReader reader) + { + var result = new List<(string TargetName, string GenericArgumentScope)> (); + foreach (var attr in GetTypeMapAssemblyTargetAttributes (reader)) { + var targetName = GetTypeMapAssemblyTargetName (reader, attr); + var memberRef = reader.GetMemberReference ((MemberReferenceHandle)attr.Constructor); + var typeSpec = reader.GetTypeSpecification ((TypeSpecificationHandle)memberRef.Parent); + var blob = reader.GetBlobReader (typeSpec.Signature); + Assert.Equal (0x15, blob.ReadByte ()); // ELEMENT_TYPE_GENERICINST + Assert.Equal (0x12, blob.ReadByte ()); // ELEMENT_TYPE_CLASS + blob.ReadCompressedInteger (); // TypeMapAssemblyTargetAttribute`1 type + Assert.Equal (1, blob.ReadCompressedInteger ()); + Assert.Equal (0x12, blob.ReadByte ()); // ELEMENT_TYPE_CLASS + var targetType = DecodeTypeDefOrRefOrSpec (blob.ReadCompressedInteger ()); + result.Add ((targetName, GetResolutionScopeName (reader, targetType))); + } + return result; + } + + static string GetTypeMapAssemblyTargetName (MetadataReader reader, CustomAttribute attr) + { + var blob = reader.GetBlobReader (attr.Value); + var prolog = blob.ReadUInt16 (); + Assert.Equal (1, prolog); // ECMA-335 custom attribute prolog + var value = blob.ReadSerializedString (); + if (value is null) { + throw new InvalidOperationException ("TypeMapAssemblyTargetAttribute value must not be null."); + } + return value; + } + + static EntityHandle DecodeTypeDefOrRefOrSpec (int codedIndex) + { + var row = codedIndex >> 2; + return (codedIndex & 0x3) switch { + 0 => MetadataTokens.TypeDefinitionHandle (row), + 1 => MetadataTokens.TypeReferenceHandle (row), + 2 => MetadataTokens.TypeSpecificationHandle (row), + _ => throw new InvalidOperationException ($"Invalid TypeDefOrRefOrSpec coded index: {codedIndex}"), + }; + } + + static string GetResolutionScopeName (MetadataReader reader, EntityHandle handle) + { + if (handle.Kind == HandleKind.TypeDefinition) { + return reader.GetString (reader.GetAssemblyDefinition ().Name); + } + if (handle.Kind != HandleKind.TypeReference) { + throw new InvalidOperationException ($"Unexpected type handle kind: {handle.Kind}"); + } + var typeReference = reader.GetTypeReference ((TypeReferenceHandle)handle); + var scope = typeReference.ResolutionScope; + return scope.Kind switch { + HandleKind.AssemblyReference => reader.GetString (reader.GetAssemblyReference ((AssemblyReferenceHandle)scope).Name), + HandleKind.ModuleDefinition => reader.GetString (reader.GetAssemblyDefinition ().Name), + HandleKind.TypeReference => GetResolutionScopeName (reader, (TypeReferenceHandle)scope), + _ => throw new InvalidOperationException ($"Unexpected resolution scope kind: {scope.Kind}"), + }; + } + [Theory] [InlineData (true)] [InlineData (false)] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 24a418fe05f..c9f070dc146 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -400,42 +400,37 @@ public void RootManifestReferencedTypes_EmptyManifest_NoChanges () } [Fact] - public void MergeCrossAssemblyAliases_RegisterTakesPrecedenceOverJniTypeSignature () + public void MergeCrossAssemblyAliases_CrossAssemblyDuplicate_FirstAssemblyOwns () { - // Java.Interop has JavaObject with [JniTypeSignature("java/lang/Object")] - var javaInteropPeer = new JavaPeerInfo { - JavaName = "java/lang/Object", CompatJniName = "java/lang/Object", - ManagedTypeName = "Java.Interop.JavaObject", ManagedTypeNamespace = "Java.Interop", ManagedTypeShortName = "JavaObject", - AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, + var firstPeer = new JavaPeerInfo { + JavaName = "com/example/Duplicate", CompatJniName = "com/example/Duplicate", + ManagedTypeName = "First.Duplicate", ManagedTypeNamespace = "First", ManagedTypeShortName = "Duplicate", + AssemblyName = "A.Binding", }; - // Mono.Android has Java.Lang.Object with [Register("java/lang/Object")] - var monoAndroidPeer = new JavaPeerInfo { - JavaName = "java/lang/Object", CompatJniName = "java/lang/Object", - ManagedTypeName = "Java.Lang.Object", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Object", - AssemblyName = "Mono.Android", IsFromJniTypeSignature = false, DoNotGenerateAcw = true, + var secondPeer = new JavaPeerInfo { + JavaName = "com/example/Duplicate", CompatJniName = "com/example/Duplicate", + ManagedTypeName = "Second.Duplicate", ManagedTypeNamespace = "Second", ManagedTypeShortName = "Duplicate", + AssemblyName = "B.Binding", }; - // Another unique peer in Java.Interop that shouldn't be moved - var otherPeer = new JavaPeerInfo { - JavaName = "java/interop/SomeHelper", CompatJniName = "java/interop/SomeHelper", - ManagedTypeName = "Java.Interop.SomeHelper", ManagedTypeNamespace = "Java.Interop", ManagedTypeShortName = "SomeHelper", - AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, + var uniquePeer = new JavaPeerInfo { + JavaName = "com/example/Unique", CompatJniName = "com/example/Unique", + ManagedTypeName = "Second.Unique", ManagedTypeNamespace = "Second", ManagedTypeShortName = "Unique", + AssemblyName = "B.Binding", }; - var allPeers = new List { javaInteropPeer, monoAndroidPeer, otherPeer }; + var allPeers = new List { firstPeer, secondPeer, uniquePeer }; var result = TrimmableTypeMapGenerator.MergeCrossAssemblyAliases (allPeers); - // Both java/lang/Object peers should be in the Mono.Android group ([Register] wins) - var monoAndroidGroup = result.Single (g => g.AssemblyName == "Mono.Android"); - Assert.Equal (2, monoAndroidGroup.Peers.Count); - Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Lang.Object"); - Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Interop.JavaObject"); + var firstGroup = result.Single (g => g.AssemblyName == "A.Binding"); + Assert.Equal (2, firstGroup.Peers.Count); + Assert.Contains (firstGroup.Peers, p => p.ManagedTypeName == "First.Duplicate"); + Assert.Contains (firstGroup.Peers, p => p.ManagedTypeName == "Second.Duplicate"); - // Java.Interop should only have the unique peer - var javaInteropGroup = result.Single (g => g.AssemblyName == "Java.Interop"); - Assert.Single (javaInteropGroup.Peers); - Assert.Equal ("Java.Interop.SomeHelper", javaInteropGroup.Peers [0].ManagedTypeName); + var secondGroup = result.Single (g => g.AssemblyName == "B.Binding"); + Assert.Single (secondGroup.Peers); + Assert.Equal ("Second.Unique", secondGroup.Peers [0].ManagedTypeName); } [Fact] @@ -481,103 +476,6 @@ public void MergeCrossAssemblyAliases_SameAssemblyAliases_NotMoved () Assert.Equal (2, result [0].Peers.Count); } - [Fact] - public void MergeCrossAssemblyAliases_SameManagedName_DifferentAssemblies_MergedCorrectly () - { - // Reproduces the java/lang/Throwable crash: two assemblies define Java.Lang.Throwable - // with the same JNI name, plus Java.Interop.JavaException also maps to the same JNI name. - // All three should be merged into the [Register]-owning assembly's group. - var javaInteropThrowable = new JavaPeerInfo { - JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", - ManagedTypeName = "Java.Lang.Throwable", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Throwable", - AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, - }; - - var monoAndroidThrowable = new JavaPeerInfo { - JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", - ManagedTypeName = "Java.Lang.Throwable", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Throwable", - AssemblyName = "Mono.Android", IsFromJniTypeSignature = false, DoNotGenerateAcw = true, - }; - - var javaException = new JavaPeerInfo { - JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", - ManagedTypeName = "Java.Interop.JavaException", ManagedTypeNamespace = "Java.Interop", ManagedTypeShortName = "JavaException", - AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, - }; - - var allPeers = new List { javaInteropThrowable, monoAndroidThrowable, javaException }; - var result = TrimmableTypeMapGenerator.MergeCrossAssemblyAliases (allPeers); - - // All java/lang/Throwable peers should be in the Mono.Android group ([Register] wins) - var monoAndroidGroup = result.Single (g => g.AssemblyName == "Mono.Android"); - Assert.Equal (3, monoAndroidGroup.Peers.Count); - Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Lang.Throwable" && p.AssemblyName == "Mono.Android"); - Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Lang.Throwable" && p.AssemblyName == "Java.Interop"); - Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Interop.JavaException"); - - // Java.Interop group should be empty (all peers moved to Mono.Android) - Assert.DoesNotContain (result, g => g.AssemblyName == "Java.Interop"); - } - - [Fact] - public void MergeCrossAssemblyAliases_SameManagedName_ProducesCorrectAliasGroup () - { - // End-to-end: after merging, ModelBuilder must produce a 3-way alias group - // for java/lang/Throwable with indexed entries and a single base entry, - // ensuring the runtime dictionary only sees java/lang/Throwable once. - var javaInteropThrowable = new JavaPeerInfo { - JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", - ManagedTypeName = "Java.Lang.Throwable", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Throwable", - AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, - }; - - var monoAndroidThrowable = new JavaPeerInfo { - JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", - ManagedTypeName = "Java.Lang.Throwable", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Throwable", - AssemblyName = "Mono.Android", IsFromJniTypeSignature = false, DoNotGenerateAcw = true, - }; - - var javaException = new JavaPeerInfo { - JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", - ManagedTypeName = "Java.Interop.JavaException", ManagedTypeNamespace = "Java.Interop", ManagedTypeShortName = "JavaException", - AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, - }; - - var allPeers = new List { javaInteropThrowable, monoAndroidThrowable, javaException }; - var merged = TrimmableTypeMapGenerator.MergeCrossAssemblyAliases (allPeers); - - // All peers should be in the Mono.Android group - Assert.Single (merged); - var group = merged [0]; - Assert.Equal ("Mono.Android", group.AssemblyName); - Assert.Equal (3, group.Peers.Count); - - // Build the model — should produce a 3-way alias group - string typeMapAssemblyName = $"_{group.AssemblyName}.TypeMap"; - var model = ModelBuilder.Build (group.Peers, typeMapAssemblyName + ".dll", typeMapAssemblyName); - - // 3 indexed entries + 1 base entry = 4 - Assert.Equal (4, model.Entries.Count); - Assert.Equal ("java/lang/Throwable[0]", model.Entries [0].JniName); - Assert.Equal ("java/lang/Throwable[1]", model.Entries [1].JniName); - Assert.Equal ("java/lang/Throwable[2]", model.Entries [2].JniName); - Assert.Equal ("java/lang/Throwable", model.Entries [3].JniName); - - // Exactly 1 alias holder - Assert.Single (model.AliasHolders); - Assert.Equal (3, model.AliasHolders [0].AliasKeys.Count); - - // The base "java/lang/Throwable" entry points to the alias holder, not a type directly - var baseEntry = model.Entries [3]; - Assert.Contains ("_Aliases", baseEntry.ProxyTypeReference); - - // 3 associations (one per peer → alias holder) - Assert.Equal (3, model.Associations.Count); - - // The bare "java/lang/Throwable" key appears exactly once — no duplicates - Assert.Single (model.Entries, e => e.JniName == "java/lang/Throwable"); - } - static PEReader CreateTestFixturePEReader () { var dir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 404d997345d..8ece123b853 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -21,6 +21,29 @@ static MemoryStream GenerateAssembly (IReadOnlyList peers, string return stream; } + static MethodDefinitionHandle FindMethodDefinition (MetadataReader reader, string methodName) => + reader.MethodDefinitions.First (h => reader.GetString (reader.GetMethodDefinition (h).Name) == methodName); + + static List FindCtorMemberRefs (MetadataReader reader, string parentNamespace, string parentName, params string [] parameterTypes) => + Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) + .Select (MetadataTokens.MemberReferenceHandle) + .Where (h => { + var member = reader.GetMemberReference (h); + if (reader.GetString (member.Name) != ".ctor" || member.Parent.Kind != HandleKind.TypeReference) + return false; + + var parent = reader.GetTypeReference ((TypeReferenceHandle) member.Parent); + if (reader.GetString (parent.Namespace) != parentNamespace || reader.GetString (parent.Name) != parentName) + return false; + + var signature = member.DecodeMethodSignature (SignatureTypeProvider.Instance, null); + return signature.ParameterTypes.SequenceEqual (parameterTypes); + }) + .ToList (); + + static MemberReferenceHandle FindCtorMemberRef (MetadataReader reader, string parentNamespace, string parentName, params string [] parameterTypes) => + FindCtorMemberRefs (reader, parentNamespace, parentName, parameterTypes).First (); + [Fact] public void Generate_ProducesValidPEAssembly () { @@ -242,25 +265,6 @@ public void Generate_EmptyPeerList_ProducesValidAssembly () Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); } - [Fact] - public void Generate_SimpleActivity_UsesGetUninitializedObject () - { - var peers = ScanFixtures (); - var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); - Assert.NotNull (simpleActivity.ActivationCtor); - Assert.NotEqual (simpleActivity.ManagedTypeName, simpleActivity.ActivationCtor.DeclaringTypeName); - - using var stream = GenerateAssembly (new [] { simpleActivity }, "InheritedCtorTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var typeNames = GetTypeRefNames (reader); - Assert.Contains ("RuntimeHelpers", typeNames); - - var memberNames = GetMemberRefNames (reader); - Assert.DoesNotContain ("CreateManagedPeer", memberNames); - Assert.Contains ("GetUninitializedObject", memberNames); - } - [Fact] public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () { @@ -284,7 +288,7 @@ public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () } [Fact] - public void Generate_InheritedCtor_UcoUsesGuardAndInlinedActivation () + public void Generate_InheritedCtor_ReferencesGuardAndActivationCtor () { var peers = ScanFixtures (); var simpleActivity = peers.First (p => p.JavaName == "my/app/SimpleActivity"); @@ -298,8 +302,42 @@ public void Generate_InheritedCtor_UcoUsesGuardAndInlinedActivation () Assert.Contains ("ShouldSkipActivation", memberNames); Assert.Contains ("GetUninitializedObject", memberNames); + Assert.DoesNotContain ("Invoke", memberNames); Assert.DoesNotContain ("ActivateInstance", memberNames); Assert.DoesNotContain ("ActivatePeerFromJavaConstructor", memberNames); + + Assert.NotEmpty (FindCtorMemberRefs (reader, "Android.App", "Activity", + "System.IntPtr", "Android.Runtime.JniHandleOwnership")); + var nctorMethodHandle = FindNctorUcoMethod (reader); + Assert.False (nctorMethodHandle.IsNil, "SimpleActivity should have a nctor_*_uco method"); + } + + [Fact] + public void Generate_InheritedJavaInteropCtor_ReferencesActivationCtor () + { + var peer = MakeAcwPeer ("test/JiInheritedTarget", "Test.JiInheritedTarget", "TestAsm") with { + ActivationCtor = new ActivationCtorInfo { + DeclaringTypeName = "Test.JiInheritedBase", + DeclaringAssemblyName = "TestAsm", + Style = ActivationCtorStyle.JavaInterop, + }, + }; + + using var stream = GenerateAssembly (new [] { peer }, "InheritedJiCtorInlineTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var typeNames = GetTypeRefNames (reader); + Assert.DoesNotContain ("MethodBase", typeNames); + + var memberNames = GetMemberRefNames (reader); + Assert.Contains ("GetUninitializedObject", memberNames); + Assert.DoesNotContain ("Invoke", memberNames); + + Assert.NotEmpty (FindCtorMemberRefs (reader, "Test", "JiInheritedBase", + "Java.Interop.JniObjectReference&", "Java.Interop.JniObjectReferenceOptions")); + var nctorMethodHandle = FindNctorUcoMethod (reader); + Assert.False (nctorMethodHandle.IsNil, "The ACW peer should have a nctor_*_uco method"); } [Fact] @@ -753,6 +791,56 @@ public void Generate_UcoMethod_BooleanParam_WrapperUsesByte_CallbackUsesSByte () Assert.Equal ("System.SByte", callbackSig.ParameterTypes.Last ()); } + [Fact] + public void Generate_UcoMethod_HasCatchRegionWithoutFinally () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + using var stream = GenerateAssembly (new [] { peer }, "UcoLegacyWrapperShape"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var ucoMethodHandle = reader.MethodDefinitions + .First (h => { + var method = reader.GetMethodDefinition (h); + var name = reader.GetString (method.Name); + return name.Contains ("onTouch") && name.Contains ("_uco_"); + }); + var ucoMethod = reader.GetMethodDefinition (ucoMethodHandle); + var body = pe.GetMethodBody (ucoMethod.RelativeVirtualAddress); + Assert.NotNull (body); + Assert.Contains (body.ExceptionRegions, r => r.Kind == ExceptionRegionKind.Catch); + Assert.DoesNotContain (body.ExceptionRegions, r => r.Kind == ExceptionRegionKind.Finally); + } + + [Fact] + public void Generate_UcoMethod_UsesDefaultUnmanagedCallersOnlyAttribute () + { + var peer = FindFixtureByJavaName ("my/app/TouchHandler"); + using var stream = GenerateAssembly (new [] { peer }, "UcoDefaultAttribute"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var ucoMethodHandle = reader.MethodDefinitions + .First (h => { + var method = reader.GetMethodDefinition (h); + var name = reader.GetString (method.Name); + return name.Contains ("onTouch") && name.Contains ("_uco_"); + }); + var attrs = reader.GetCustomAttributes (ucoMethodHandle) + .Select (h => reader.GetCustomAttribute (h)) + .Where (attr => attr.Constructor.Kind == HandleKind.MemberReference) + .Where (attr => { + var ctor = reader.GetMemberReference ((MemberReferenceHandle) attr.Constructor); + if (ctor.Parent.Kind != HandleKind.TypeReference) + return false; + var type = reader.GetTypeReference ((TypeReferenceHandle) ctor.Parent); + return reader.GetString (type.Name) == "UnmanagedCallersOnlyAttribute"; + }) + .ToList (); + var ucoAttr = Assert.Single (attrs); + Assert.Equal (new byte [] { 0x01, 0x00, 0x00, 0x00 }, reader.GetBlobBytes (ucoAttr.Value)); + } + static MemberReference FindCallbackMemberRef (MetadataReader reader, string methodName) { var refs = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) @@ -1022,10 +1110,8 @@ public void Generate_AliasHolder_HasDeserializableAliasKeys () } [Fact] - public void Generate_UcoConstructor_BodyUsesMarshalMethodPattern () + public void Generate_UcoConstructor_HasMarshalMethodMetadataAndExceptionRegions () { - // Verify that UCO constructor bodies wrap activation in BeginMarshalMethod/EndMarshalMethod - // with try/catch/finally so that exceptions cannot cross the JNI boundary (causing SIGABRT). var peer = MakeAcwPeer ("test/UcoCtorExc", "Test.UcoCtorExc", "TestAsm"); using var stream = GenerateAssembly (new [] { peer }, "UcoCtorMarshalTest"); using var pe = new PEReader (stream); @@ -1056,27 +1142,6 @@ public void Generate_UcoConstructor_BodyUsesMarshalMethodPattern () $"UCO constructor should have at least 2 exception regions (catch + finally), found {regions.Length}"); Assert.Contains (regions, r => r.Kind == ExceptionRegionKind.Catch); Assert.Contains (regions, r => r.Kind == ExceptionRegionKind.Finally); - - // Verify the method body IL actually calls the marshal-method APIs (not just that the refs exist in the assembly). - var il = pe.GetSectionData (nctorMethod.RelativeVirtualAddress); - var ilBytes = body.GetILBytes (); - Assert.NotNull (ilBytes); - var ilContent = System.Text.Encoding.ASCII.GetString (ilBytes); - // Cross-check: the member refs we found must be referenced from within this method body. - // We verify by checking that the IL contains Call/Callvirt opcodes (0x28/0x6F) with tokens - // pointing to the expected member refs. - var memberRefHandles = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.MemberRef)) - .Select (i => MetadataTokens.MemberReferenceHandle (i)) - .ToList (); - var beginHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "BeginMarshalMethod"); - var endHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "EndMarshalMethod"); - var exHandle = memberRefHandles.First (h => reader.GetString (reader.GetMemberReference (h).Name) == "OnUserUnhandledException"); - int beginToken = MetadataTokens.GetToken (beginHandle); - int endToken = MetadataTokens.GetToken (endHandle); - int exToken = MetadataTokens.GetToken (exHandle); - Assert.True (ILContainsCallToken (ilBytes, beginToken), "nctor_*_uco IL should call BeginMarshalMethod"); - Assert.True (ILContainsCallToken (ilBytes, endToken), "nctor_*_uco IL should call EndMarshalMethod"); - Assert.True (ILContainsCallToken (ilBytes, exToken), "nctor_*_uco IL should call OnUserUnhandledException"); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 651f1ea3c40..15331e47fb4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -458,16 +458,17 @@ public void Fixture_InterfaceAndInvoker_ShareJniName_InvokerSeparated () var model = BuildModel (clickPeers, "TypeMap"); - // Invoker is excluded entirely — no TypeMap entry, no proxy. - // Only the interface gets a TypeMap entry and a proxy. + // Invoker is excluded from TypeMap entries/proxies. It still gets a + // managed→proxy association so its JniPeerMembers can resolve the JNI name. Assert.Single (model.Entries); Assert.Equal ("android/view/View$OnClickListener", model.Entries [0].JniName); - // Only the interface proxy exists; the invoker type is referenced - // only as a TypeRef in the interface proxy's InvokerType property. + // Only the interface proxy exists; the invoker type is also referenced + // as a TypeRef in the interface proxy's InvokerType property. Assert.Single (model.ProxyTypes); Assert.NotNull (model.ProxyTypes [0].InvokerType); Assert.Equal ("Android.Views.IOnClickListenerInvoker", model.ProxyTypes [0].InvokerType!.ManagedTypeName); + Assert.Contains (model.Associations, a => a.SourceTypeReference == "Android.Views.IOnClickListenerInvoker, TestFixtures"); } [Fact] @@ -493,6 +494,10 @@ public void Build_InvokerType_NoProxyNoEntry () // Interface proxy has activation because it will create the invoker Assert.True (proxy.HasActivation); + + Assert.Equal (2, model.Associations.Count); + Assert.Contains (model.Associations, a => a.SourceTypeReference == "MyApp.IFoo, App"); + Assert.Contains (model.Associations, a => a.SourceTypeReference == "MyApp.FooInvoker, App"); } } @@ -659,6 +664,7 @@ public void Build_TypeIsInvoker_OnlyWhenReferencedByAnotherPeer () // Only the interface gets entries/proxies, the invoker is excluded Assert.Single (model2.Entries); Assert.Equal ("MyApp.IMyInterface", model2.ProxyTypes [0].TargetType.ManagedTypeName); + Assert.Contains (model2.Associations, a => a.SourceTypeReference == "MyApp.MyInvoker, App"); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs index b1f96d6d320..52786546f22 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.EdgeCases.cs @@ -41,6 +41,14 @@ public void Scan_NestedType_HasCorrectNamespace (string managedName, string expe Assert.Equal (expectedNamespace, FindFixtureByManagedName (managedName).ManagedTypeNamespace); } + [Fact] + public void Scan_RegisterConnectorNestedInvoker_UsesMetadataNestedTypeName () + { + var peer = FindFixtureByManagedName ("Android.App.Application+IActivityLifecycleCallbacks"); + + Assert.Equal ("Android.App.Application+IActivityLifecycleCallbacksInvoker", peer.InvokerTypeName); + } + [Fact] public void Scan_EmptyNamespace_Handled () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index fddfaae55a9..4864ff291cd 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -54,6 +54,17 @@ public void Scan_TypeMetadata_IsCorrect () Assert.Equal ("MyApp.Generic.GenericHolder`1", generic.ManagedTypeName); } + [Fact] + public void Scan_ContentProvider_CapturesAuthorities () + { + var provider = FindFixtureByJavaName ("my/app/MyProvider"); + var component = provider.ComponentAttribute; + Assert.NotNull (component); + Assert.Equal (ComponentKind.ContentProvider, component.Kind); + Assert.True (component.Properties.TryGetValue ("Authorities", out var authorities)); + Assert.Equal ("my.app.provider", authorities); + } + [Fact] public void Scan_InvokerAndInterface_ShareJavaName () { @@ -116,8 +127,7 @@ public void Scan_JniTypeSignature_DuplicateJniName_BothPresent () { // Java.Interop.TestTypes.JavaObject has [JniTypeSignature("java/lang/Object", GenerateJavaPeer=false)] // and Java.Lang.Object has [Register("java/lang/Object", DoNotGenerateAcw=true)]. - // Both should be present in the scan results — alias support (PR #11122) handles - // the runtime deduplication. + // Both should be present in the scan results — alias support handles the runtime deduplication. var peers = ScanFixtures (); var javaObjectPeers = peers.Where (p => p.JavaName == "java/lang/Object").ToList (); Assert.Equal (2, javaObjectPeers.Count); @@ -126,8 +136,7 @@ public void Scan_JniTypeSignature_DuplicateJniName_BothPresent () [Fact] public void Scan_JniTypeSignature_SubclassExtendsJavaPeer () { - // JavaDisposedObject extends JavaObject which has [JniTypeSignature(GenerateJavaPeer=false)] - // The scanner should still detect JavaDisposedObject as extending a Java peer + // JavaDisposedObject extends JavaObject which has [JniTypeSignature(GenerateJavaPeer=false)]. var peer = FindFixtureByJavaName ("net/dot/jni/test/JavaDisposedObject"); Assert.NotNull (peer); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index a4363fe9877..7e8111cfd24 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -55,6 +55,20 @@ public class Application : Java.Lang.Object { public Application () { } protected Application (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("android/app/Application$ActivityLifecycleCallbacks", "", "Android.App.Application/IActivityLifecycleCallbacksInvoker")] + public interface IActivityLifecycleCallbacks + { + [Register ("onActivityCreated", "(Landroid/app/Activity;)V", "GetOnActivityCreated_Landroid_app_Activity_Handler:Android.App.Application/IActivityLifecycleCallbacksInvoker")] + void OnActivityCreated (Activity activity); + } + + [Register ("android/app/Application$ActivityLifecycleCallbacks", DoNotGenerateAcw = true)] + internal sealed class IActivityLifecycleCallbacksInvoker : Java.Lang.Object, IActivityLifecycleCallbacks + { + public IActivityLifecycleCallbacksInvoker (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + public void OnActivityCreated (Activity activity) { } + } } [Register ("android/app/Instrumentation", DoNotGenerateAcw = true)] diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj index 13139b23246..11426b511b1 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj @@ -38,7 +38,10 @@ - + + + Java.Interop.GenericMarshaler\JniPeerInstanceMethodsExtensions.cs + diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs index 233cc67fa23..54459642bd7 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs @@ -1,6 +1,9 @@ using System; using System.Collections.Concurrent; +using System.Collections.Generic; using System.Reflection; +using System.Threading; +using System.Threading.Tasks; using Android.Runtime; using Java.Interop; using Microsoft.Android.Runtime; @@ -18,37 +21,17 @@ sealed class TestableTrimmableTypeMapTypeManager : TrimmableTypeMapTypeManager { } - [Test] - public void GetStaticMethodFallbackTypes_WithPackageName_ReturnsDesugarFallbacks () + [TestCase ("android/app/Activity", "android/app/DesugarActivity$_CC", "android/app/Activity$-CC")] + [TestCase ("Activity", "DesugarActivity$_CC", "Activity$-CC")] + [TestCase ("com/example/package/MyInterface", "com/example/package/DesugarMyInterface$_CC", "com/example/package/MyInterface$-CC")] + public void GetStaticMethodFallbackTypes_ReturnsDesugarFallbacks (string jniSimpleReference, string expectedDesugar, string expectedFallback) { using var manager = new TestableTrimmableTypeMapTypeManager (); - var fallbacks = manager.GetStaticMethodFallbackTypes ("android/app/Activity"); - Assert.IsNotNull (fallbacks); - Assert.AreEqual (2, fallbacks!.Count); - Assert.AreEqual ("android/app/DesugarActivity$_CC", fallbacks [0]); - Assert.AreEqual ("android/app/Activity$-CC", fallbacks [1]); - } + var fallbacks = GetStaticMethodFallbackTypes (manager, jniSimpleReference); - [Test] - public void GetStaticMethodFallbackTypes_WithoutPackageName_ReturnsDesugarFallbacks () - { - using var manager = new TestableTrimmableTypeMapTypeManager (); - var fallbacks = manager.GetStaticMethodFallbackTypes ("Activity"); - Assert.IsNotNull (fallbacks); - Assert.AreEqual (2, fallbacks!.Count); - Assert.AreEqual ("DesugarActivity$_CC", fallbacks [0]); - Assert.AreEqual ("Activity$-CC", fallbacks [1]); - } - - [Test] - public void GetStaticMethodFallbackTypes_WithDeepPackageName_ReturnsDesugarFallbacks () - { - using var manager = new TestableTrimmableTypeMapTypeManager (); - var fallbacks = manager.GetStaticMethodFallbackTypes ("com/example/package/MyInterface"); - Assert.IsNotNull (fallbacks); - Assert.AreEqual (2, fallbacks!.Count); - Assert.AreEqual ("com/example/package/DesugarMyInterface$_CC", fallbacks [0]); - Assert.AreEqual ("com/example/package/MyInterface$-CC", fallbacks [1]); + Assert.AreEqual (2, fallbacks.Count); + Assert.AreEqual (expectedDesugar, fallbacks [0]); + Assert.AreEqual (expectedFallback, fallbacks [1]); } // Verifies the generic-type-definition fallback in GetProxyForManagedType: @@ -57,9 +40,7 @@ public void GetStaticMethodFallbackTypes_WithDeepPackageName_ReturnsDesugarFallb [Test] public void TryGetJniNameForManagedType_ClosedGeneric_ResolvesViaGenericTypeDefinition () { - if (!RuntimeFeature.TrimmableTypeMap) { - Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); - } + AssumeTrimmableTypeMapEnabled (); var instance = TrimmableTypeMap.Instance; @@ -79,9 +60,7 @@ public void TryGetJniNameForManagedType_ClosedGeneric_ResolvesViaGenericTypeDefi [Test] public void TryGetJniNameForManagedType_NonGenericType_ResolvesDirectly () { - if (!RuntimeFeature.TrimmableTypeMap) { - Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); - } + AssumeTrimmableTypeMapEnabled (); // Regression: the GTD fallback must not disturb the non-generic hot path. Assert.IsTrue (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (typeof (JavaList), out var jniName)); @@ -91,9 +70,7 @@ public void TryGetJniNameForManagedType_NonGenericType_ResolvesDirectly () [Test] public void TryGetJniNameForManagedType_UnknownClosedGeneric_ReturnsFalse () { - if (!RuntimeFeature.TrimmableTypeMap) { - Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); - } + AssumeTrimmableTypeMapEnabled (); // System.Collections.Generic.List has no TypeMapAssociation — both the // direct lookup AND the GTD fallback must miss, and the API must return false. @@ -105,9 +82,7 @@ public void TryGetJniNameForManagedType_UnknownClosedGeneric_ReturnsFalse () [Test] public void TryGetJniNameForManagedType_RepeatedClosedGenericLookup_IsCached () { - if (!RuntimeFeature.TrimmableTypeMap) { - Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); - } + AssumeTrimmableTypeMapEnabled (); // Closed generic peers normalize to their open generic definition, so // repeated lookups reuse the same cached proxy. @@ -121,9 +96,7 @@ public void TryGetJniNameForManagedType_RepeatedClosedGenericLookup_IsCached () [Test] public void TryGetJniNameForManagedType_DifferentClosedGenerics_UseGenericDefinitionCacheKey () { - if (!RuntimeFeature.TrimmableTypeMap) { - Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); - } + AssumeTrimmableTypeMapEnabled (); var instance = TrimmableTypeMap.Instance; var cache = GetProxyCache (instance); @@ -140,6 +113,72 @@ public void TryGetJniNameForManagedType_DifferentClosedGenerics_UseGenericDefini Assert.IsFalse (cache.ContainsKey (typeof (JavaList))); } + [Test] + public void RegisteredPeer_Dispose_InvokesDisposing () + { + AssumeTrimmableTypeMapEnabled (); + + bool disposed = false; + bool finalized = false; + var value = new TrimmableRegisteredDisposedObject { + OnDisposed = () => disposed = true, + OnFinalized = () => finalized = true, + }; + + value.Dispose (); + + Assert.IsTrue (disposed); + Assert.IsFalse (finalized); + } + + [Test] + public async Task RegisteredPeer_Dispose_Finalized () + { + AssumeTrimmableTypeMapEnabled (); + + var disposed = new TaskCompletionSource (TaskCreationOptions.RunContinuationsAsynchronously); + var finalized = new TaskCompletionSource (TaskCreationOptions.RunContinuationsAsynchronously); + + PerformNoPinAction (() => { + PerformNoPinAction (() => { + var value = new TrimmableRegisteredDisposedObject { + OnDisposed = () => disposed.TrySetResult (true), + OnFinalized = () => finalized.TrySetResult (true), + }; + GC.KeepAlive (value); + }); + JniEnvironment.Runtime.ValueManager.CollectPeers (); + }); + JniEnvironment.Runtime.ValueManager.CollectPeers (); + + await WaitForGC (() => disposed.Task.IsCompleted || finalized.Task.IsCompleted, + "Expected TrimmableRegisteredDisposedObject.Dispose(disposing: false) to run."); + + Assert.IsFalse (disposed.Task.IsCompleted); + Assert.IsTrue (finalized.Task.IsCompleted); + } + + [Test] + public void RegisteredPeer_NestedDisposeInvocations () + { + AssumeTrimmableTypeMapEnabled (); + + var value = new TrimmableRegisteredNestedDisposableObject (); + value.Dispose (); + value.Dispose (); + } + + [Test] + public void RegisteredPeer_CanCreateGenericHolder () + { + AssumeTrimmableTypeMapEnabled (); + + using var holder = new TrimmableRegisteredGenericHolder (); + holder.Value = 42; + + Assert.AreEqual (42, holder.Value); + } + static ConcurrentDictionary GetProxyCache (TrimmableTypeMap instance) { var field = typeof (TrimmableTypeMap).GetField ("_proxyCache", BindingFlags.Instance | BindingFlags.NonPublic); @@ -156,6 +195,55 @@ static ConcurrentDictionary GetProxyCache (TrimmableTypeMap throw new InvalidOperationException ("Unable to access TrimmableTypeMap proxy cache."); } + static IReadOnlyList GetStaticMethodFallbackTypes (TestableTrimmableTypeMapTypeManager manager, string jniSimpleReference) + { + var fallbacks = manager.GetStaticMethodFallbackTypes (jniSimpleReference); + Assert.IsNotNull (fallbacks); + return fallbacks ?? throw new InvalidOperationException ("Expected fallback types."); + } + + static void AssumeTrimmableTypeMapEnabled () + { + if (!RuntimeFeature.TrimmableTypeMap) { + Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); + } + } + + static async Task WaitForGC (Func predicate, string message, int timeoutMilliseconds = 2000) + { + var timeout = TimeSpan.FromMilliseconds (timeoutMilliseconds); + var start = DateTime.UtcNow; + while (!predicate () && DateTime.UtcNow - start < timeout) { + GC.Collect (generation: 2, mode: GCCollectionMode.Forced, blocking: true); + GC.WaitForPendingFinalizers (); + JniEnvironment.Runtime.ValueManager.CollectPeers (); + await Task.Yield (); + } + Assert.IsTrue (predicate (), message); + } + + static IntPtr noPinActionPointer; + + static unsafe void NoPinActionHelper (int depth, Action action) + { + int* values = stackalloc int [20]; + noPinActionPointer = new IntPtr (values); + + if (depth <= 0) { + new object (); + action (); + } else { + NoPinActionHelper (depth - 1, action); + } + } + + static void PerformNoPinAction (Action action) + { + var thread = new Thread (() => NoPinActionHelper (128, action)); + thread.Start (); + thread.Join (); + } + // Pure-function tests for the TargetTypeMatches helper used by // TryGetProxyFromHierarchy when the hierarchy lookup finds a proxy whose // stored TargetType is an open generic definition. @@ -207,4 +295,53 @@ public void TargetTypeMatches_UnrelatedNonGeneric_ReturnsFalse () Assert.IsFalse (TrimmableTypeMap.TargetTypeMatches (typeof (string), typeof (int))); } } + + [Register ("net/dot/android/test/TrimmableRegisteredDisposedObject")] + class TrimmableRegisteredDisposedObject : Java.Lang.Object + { + public Action OnDisposed = delegate { }; + public Action OnFinalized = delegate { }; + + public TrimmableRegisteredDisposedObject () + { + } + + protected override void Dispose (bool disposing) + { + if (disposing) { + OnDisposed (); + } else { + OnFinalized (); + } + base.Dispose (disposing); + } + } + + [Register ("net/dot/android/test/TrimmableRegisteredNestedDisposableObject")] + class TrimmableRegisteredNestedDisposableObject : Java.Lang.Object + { + bool isDisposed; + + public TrimmableRegisteredNestedDisposableObject () + { + } + + protected override void Dispose (bool disposing) + { + if (isDisposed) { + return; + } + isDisposed = true; + if (Handle != IntPtr.Zero) { + Dispose (); + } + base.Dispose (disposing); + } + } + + [Register ("net/dot/android/test/TrimmableRegisteredGenericHolder")] + class TrimmableRegisteredGenericHolder : Java.Lang.Object + { + public T Value { get; set; } + } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index e3e89a38cff..6b686ecb8a6 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -29,7 +29,7 @@ NetworkInterfaces excluded: https://github.com/dotnet/runtime/issues/75155 --> - $(ExcludeCategories):CoreCLRIgnore:NTLM + $(ExcludeCategories):CoreCLRIgnore:NTLM $(ExcludeCategories):NativeAOTIgnore:SSL:NTLM:AndroidClientHandler:Export:NativeTypeMap @@ -74,17 +74,10 @@ - - - - - - - - + + + <_AndroidRemapMembers Include="Remaps.xml" /> <_AndroidRemapMembers Include="IsAssignableFromRemaps.xml" Condition=" '$(_AndroidIsAssignableFromCheck)' == 'false' " /> diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/StartupHookRoots.xml b/tests/Mono.Android-Tests/Mono.Android-Tests/StartupHookRoots.xml new file mode 100644 index 00000000000..59a0ba9e565 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/StartupHookRoots.xml @@ -0,0 +1,5 @@ + + + + + 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 f3e73568eed..b2a9ecabeeb 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 @@ -53,6 +53,10 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateValue", "Java.InteropTests.JniValueMarshaler_object_ContractTests.SpecificTypesAreUsed", + // net.dot.jni.test.GetThis static init — same JavaProxy* + // root cause as the JavaProxyObject exclusions above. + "Java.InteropTests.JavaObjectTest.DisposeAccessesThis", + // net.dot.jni.internal.JavaProxyThrowable static init — same JavaProxy* // root cause as the JavaProxyObject exclusions above. "Java.InteropTests.JavaExceptionTests.InnerExceptionIsNotAProxy", @@ -66,12 +70,6 @@ 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 — - // 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", - // Open generic type handling differs from non-trimmable "Java.InteropTests.JnienvTest.NewOpenGenericTypeThrows",