diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 01f29e322c1..e1763c9dd7f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -1,22 +1,52 @@ using System; using System.Collections.Generic; using System.IO; +using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// -/// Generates the root _Microsoft.Android.TypeMaps.dll assembly that references -/// all per-assembly typemap assemblies via -/// [assembly: TypeMapAssemblyTargetAttribute<Java.Lang.Object>("name")]. +/// Generates the root _Microsoft.Android.TypeMaps.dll assembly that: +/// +/// References all per-assembly typemap assemblies via [assembly: TypeMapAssemblyTargetAttribute<__TypeMapAnchor>("name")]. +/// Emits a StartupHook class whose Initialize() method calls +/// with the appropriate +/// type mapping dictionaries. +/// /// /// /// The generated assembly looks like this (pseudo-C#): /// -/// // One attribute per per-assembly typemap assembly — tells the runtime where to find TypeMap entries: -/// [assembly: TypeMapAssemblyTarget<Java.Lang.Object>("_Mono.Android.TypeMap")] -/// [assembly: TypeMapAssemblyTarget<Java.Lang.Object>("_MyApp.TypeMap")] +/// internal class __TypeMapAnchor { } +/// +/// // One attribute per per-assembly typemap assembly: +/// [assembly: TypeMapAssemblyTarget<__TypeMapAnchor>("_Mono.Android.TypeMap")] +/// [assembly: TypeMapAssemblyTarget<__TypeMapAnchor>("_MyApp.TypeMap")] +/// +/// // Startup hook — called by DOTNET_STARTUP_HOOKS: +/// internal static class StartupHook +/// { +/// internal static void Initialize () +/// { +/// // Option A: Shared universe +/// TrimmableTypeMap.Initialize( +/// TypeMapping.GetOrCreateExternalTypeMapping<Java.Lang.Object>(), +/// TypeMapping.GetOrCreateProxyTypeMapping<Java.Lang.Object>()); +/// +/// // Option B: Per-assembly universes (aggregated) +/// var typeMaps = new IReadOnlyDictionary<string, Type>[] { +/// TypeMapping.GetOrCreateExternalTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>(), +/// TypeMapping.GetOrCreateExternalTypeMapping<_MyApp_TypeMap.__TypeMapAnchor>(), +/// }; +/// var proxyMaps = new IReadOnlyDictionary<Type, Type>[] { +/// TypeMapping.GetOrCreateProxyTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>(), +/// TypeMapping.GetOrCreateProxyTypeMapping<_MyApp_TypeMap.__TypeMapAnchor>(), +/// }; +/// TrimmableTypeMap.Initialize(typeMaps, proxyMaps); +/// } +/// } /// /// public sealed class RootTypeMapAssemblyGenerator @@ -35,10 +65,11 @@ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion) /// Generates the root typemap assembly and writes it to the given stream. /// /// Names of per-assembly typemap assemblies to reference. + /// True to merge all assemblies into a single typemap universe, false for per-assembly universes. /// Stream to write the output PE to. /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). /// Optional module name for the PE metadata. - public void Generate (IReadOnlyList perAssemblyTypeMapNames, Stream stream, string? assemblyName = null, string? moduleName = null) + public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null) { if (perAssemblyTypeMapNames is null) { throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); @@ -53,30 +84,298 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, Stream stre var pe = new PEAssemblyBuilder (_systemRuntimeVersion); pe.EmitPreamble (assemblyName, moduleName); - // Reference the open generic TypeMapAssemblyTargetAttribute`1 from System.Runtime.InteropServices + EntityHandle anchorTypeHandle; + if (useSharedTypemapUniverse) { + // In merged mode, all per-assembly typemaps use Java.Lang.Object as the shared + // anchor type, so the root assembly must also use Java.Lang.Object. + anchorTypeHandle = pe.Metadata.AddTypeReference (pe.MonoAndroidRef, + pe.Metadata.GetOrAddString ("Java.Lang"), + pe.Metadata.GetOrAddString ("Object")); + } else { + // In aggregate mode, each per-assembly typemap has its own __TypeMapAnchor. + // The root also defines its own for TypeMapAssemblyTargetAttribute grouping. + var objectRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeRef, + pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object")); + anchorTypeHandle = pe.Metadata.AddTypeDefinition ( + TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Class, + default, + pe.Metadata.GetOrAddString ("__TypeMapAnchor"), + objectRef, + MetadataTokens.FieldDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1)); + } + + // Emit [assembly: TypeMapAssemblyTargetAttribute<__TypeMapAnchor>("name")] for each per-assembly typemap + EmitAssemblyTargetAttributes (pe, anchorTypeHandle, perAssemblyTypeMapNames); + + // Emit [assembly: IgnoresAccessChecksTo("...")] so the startup hook can access + // internal types (SingleUniverseTypeMap, AggregateTypeMap, TrimmableTypeMap in Mono.Android, + // and __TypeMapAnchor in each per-assembly typemap DLL). + var accessTargets = new List { "Mono.Android" }; + if (!useSharedTypemapUniverse) { + accessTargets.AddRange (perAssemblyTypeMapNames); + } + pe.EmitIgnoresAccessChecksToAttribute (accessTargets); + + // Emit StartupHook class with Initialize() method + EmitStartupHook (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse); + + pe.WritePE (stream); + } + + static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames) + { var openAttrRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeInteropServicesRef, pe.Metadata.GetOrAddString ("System.Runtime.InteropServices"), pe.Metadata.GetOrAddString ("TypeMapAssemblyTargetAttribute`1")); - // Reference Java.Lang.Object from Mono.Android (the type universe) - var javaLangObjectRef = pe.Metadata.AddTypeReference (pe.MonoAndroidRef, - pe.Metadata.GetOrAddString ("Java.Lang"), pe.Metadata.GetOrAddString ("Object")); - - // Build TypeSpec for TypeMapAssemblyTargetAttribute - var closedAttrTypeSpec = pe.MakeGenericTypeSpec (openAttrRef, javaLangObjectRef); + var closedAttrTypeSpec = pe.MakeGenericTypeSpec (openAttrRef, anchorTypeHandle); - // MemberRef for .ctor(string) on the closed generic type var ctorRef = pe.AddMemberRef (closedAttrTypeSpec, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, rt => rt.Void (), p => p.AddParameter ().Type ().String ())); - // Add [assembly: TypeMapAssemblyTargetAttribute("name")] for each per-assembly typemap foreach (var name in perAssemblyTypeMapNames) { var blobHandle = pe.BuildAttributeBlob (blob => blob.WriteSerializedString (name)); pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle); } + } - pe.WritePE (stream); + static void EmitStartupHook (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse) + { + var metadata = pe.Metadata; + + // Type references + var iReadOnlyDictOpenRef = metadata.AddTypeReference (pe.SystemRuntimeRef, + metadata.GetOrAddString ("System.Collections.Generic"), metadata.GetOrAddString ("IReadOnlyDictionary`2")); + var systemTypeRef = metadata.AddTypeReference (pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type")); + + var trimmableTypeMapRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("TrimmableTypeMap")); + + var typeMappingRef = metadata.AddTypeReference (pe.SystemRuntimeInteropServicesRef, + metadata.GetOrAddString ("System.Runtime.InteropServices"), metadata.GetOrAddString ("TypeMapping")); + + // Build MemberRefs for TypeMapping methods using manual signature encoding + // TypeMapping.GetOrCreateExternalTypeMapping() returns IReadOnlyDictionary + var getExternalMemberRef = AddTypeMappingMethodRef (pe, typeMappingRef, "GetOrCreateExternalTypeMapping", + iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + + // TypeMapping.GetOrCreateProxyTypeMapping() returns IReadOnlyDictionary + var getProxyMemberRef = AddTypeMappingMethodRef (pe, typeMappingRef, "GetOrCreateProxyTypeMapping", + iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + + // Define the StartupHook type + metadata.AddTypeDefinition ( + TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Abstract | TypeAttributes.Class, + default, + metadata.GetOrAddString ("StartupHook"), + metadata.AddTypeReference (pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Object")), + MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); + + if (useSharedTypemapUniverse) { + // TrimmableTypeMap.Initialize(IReadOnlyDictionary, IReadOnlyDictionary) + var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); + } else { + // TrimmableTypeMap.Initialize(IReadOnlyDictionary[], IReadOnlyDictionary[]) + var initializeRef = AddInitializeAggregateRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef); + } + } + + static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, + MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, + MemberReferenceHandle initializeRef) + { + var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); + var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); + + pe.EmitBody ("Initialize", + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), + encoder => { + // TypeMapping.GetOrCreateExternalTypeMapping<__TypeMapAnchor>() + encoder.OpCode (ILOpCode.Call); + encoder.Token (getExternalSpec); + // TypeMapping.GetOrCreateProxyTypeMapping<__TypeMapAnchor>() + encoder.OpCode (ILOpCode.Call); + encoder.Token (getProxySpec); + // TrimmableTypeMap.Initialize(typeMap, proxyMap) + encoder.OpCode (ILOpCode.Call); + encoder.Token (initializeRef); + encoder.OpCode (ILOpCode.Ret); + }); + } + + static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, + IReadOnlyList perAssemblyTypeMapNames, + MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, + MemberReferenceHandle initializeRef, + TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + { + var count = perAssemblyTypeMapNames.Count; + + var getExternalSpecs = new EntityHandle [count]; + var getProxySpecs = new EntityHandle [count]; + for (int i = 0; i < count; i++) { + var asmRef = pe.FindOrAddAssemblyRef (perAssemblyTypeMapNames [i]); + var perAsmAnchorRef = pe.Metadata.AddTypeReference (asmRef, + default, pe.Metadata.GetOrAddString ("__TypeMapAnchor")); + getExternalSpecs [i] = MakeGenericMethodSpec (pe, getExternalMemberRef, perAsmAnchorRef); + getProxySpecs [i] = MakeGenericMethodSpec (pe, getProxyMemberRef, perAsmAnchorRef); + } + + pe.EmitBody ("Initialize", + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), + encoder => { + // var typeMaps = new IReadOnlyDictionary[N]; + encoder.LoadConstantI4 (count); + encoder.OpCode (ILOpCode.Newarr); + encoder.Token (externalDictTypeSpec); + encoder.OpCode (ILOpCode.Stloc_0); + + for (int i = 0; i < count; i++) { + encoder.OpCode (ILOpCode.Ldloc_0); + encoder.LoadConstantI4 (i); + encoder.OpCode (ILOpCode.Call); + encoder.Token (getExternalSpecs [i]); + encoder.OpCode (ILOpCode.Stelem_ref); + } + + // var proxyMaps = new IReadOnlyDictionary[N]; + encoder.LoadConstantI4 (count); + encoder.OpCode (ILOpCode.Newarr); + encoder.Token (proxyDictTypeSpec); + encoder.OpCode (ILOpCode.Stloc_1); + + for (int i = 0; i < count; i++) { + encoder.OpCode (ILOpCode.Ldloc_1); + encoder.LoadConstantI4 (i); + encoder.OpCode (ILOpCode.Call); + encoder.Token (getProxySpecs [i]); + encoder.OpCode (ILOpCode.Stelem_ref); + } + + // TrimmableTypeMap.Initialize(typeMaps, proxyMaps) + encoder.OpCode (ILOpCode.Ldloc_0); + encoder.OpCode (ILOpCode.Ldloc_1); + encoder.OpCode (ILOpCode.Call); + encoder.Token (initializeRef); + encoder.OpCode (ILOpCode.Ret); + }, + encodeLocals: localsSig => { + // LOCAL_SIG header + 2 locals + localsSig.WriteByte (0x07); // LOCAL_SIG + localsSig.WriteCompressedInteger (2); // count + // local 0: IReadOnlyDictionary[] + localsSig.WriteByte (0x1D); // SZARRAY + EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + // local 1: IReadOnlyDictionary[] + localsSig.WriteByte (0x1D); // SZARRAY + EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + }); + } + + /// + /// Creates a MethodSpec for a generic method instantiation with a specific type argument. + /// + static MethodSpecificationHandle MakeGenericMethodSpec (PEAssemblyBuilder pe, MemberReferenceHandle openMethodRef, EntityHandle typeArg) + { + var blob = new BlobBuilder (16); + blob.WriteByte (0x0A); // GENMETHOD_INST + blob.WriteCompressedInteger (1); // generic arity + blob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeArg)); + return pe.Metadata.AddMethodSpecification (openMethodRef, pe.Metadata.GetOrAddBlob (blob)); + } + + /// + /// Creates a MemberRef for a TypeMapping generic method with the correct return type signature. + /// The method signature is: generic arity 1, 0 params, returns IReadOnlyDictionary<K, V>. + /// + static MemberReferenceHandle AddTypeMappingMethodRef (PEAssemblyBuilder pe, TypeReferenceHandle typeMappingRef, string methodName, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, bool keyIsString) + { + var blob = new BlobBuilder (64); + // Method signature: GENERIC, arity=1, param count=0, return type + blob.WriteByte (0x10); // IMAGE_CEE_CS_CALLCONV_GENERIC + blob.WriteCompressedInteger (1); // generic parameter count + blob.WriteCompressedInteger (0); // parameter count + // Return type: IReadOnlyDictionary + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString); + return pe.Metadata.AddMemberReference (typeMappingRef, + pe.Metadata.GetOrAddString (methodName), pe.Metadata.GetOrAddBlob (blob)); + } + + /// + /// Creates a MemberRef for TrimmableTypeMap.Initialize(IReadOnlyDictionary<string, Type>, IReadOnlyDictionary<Type, Type>). + /// + static MemberReferenceHandle AddInitializeSingleRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + { + var blob = new BlobBuilder (64); + blob.WriteByte (0x00); // DEFAULT (static) + blob.WriteCompressedInteger (2); // parameter count + blob.WriteByte (0x01); // return type: void + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + return pe.Metadata.AddMemberReference (trimmableTypeMapRef, + pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); + } + + /// + /// Creates a MemberRef for TrimmableTypeMap.Initialize(IReadOnlyDictionary<string, Type>[], IReadOnlyDictionary<Type, Type>[]). + /// + static MemberReferenceHandle AddInitializeAggregateRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + { + var blob = new BlobBuilder (64); + blob.WriteByte (0x00); // DEFAULT (static) + blob.WriteCompressedInteger (2); // parameter count + blob.WriteByte (0x01); // return type: void + // Param 1: IReadOnlyDictionary[] + blob.WriteByte (0x1D); // SZARRAY + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + // Param 2: IReadOnlyDictionary[] + blob.WriteByte (0x1D); // SZARRAY + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + return pe.Metadata.AddMemberReference (trimmableTypeMapRef, + pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); + } + + /// + /// Creates a TypeSpec for a closed IReadOnlyDictionary<K, V> generic type (for newarr). + /// + static TypeSpecificationHandle MakeIReadOnlyDictTypeSpec (PEAssemblyBuilder pe, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, bool keyIsString) + { + var blob = new BlobBuilder (32); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString); + return pe.Metadata.AddTypeSpecification (pe.Metadata.GetOrAddBlob (blob)); + } + + static void EncodeIReadOnlyDictType (BlobBuilder blob, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, bool keyIsString) + { + blob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST + blob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (iReadOnlyDictOpenRef)); + blob.WriteCompressedInteger (2); // generic arity = 2 + if (keyIsString) { + blob.WriteByte (0x0E); // ELEMENT_TYPE_STRING + } else { + blob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (systemTypeRef)); + } + blob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (systemTypeRef)); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 9bb04468b6c..59c01a00bab 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -14,10 +14,12 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// The generated assembly looks like this (pseudo-C#): /// -/// // Assembly-level TypeMap attributes — one per Java peer type: -/// [assembly: TypeMap<Java.Lang.Object>("android/app/Activity", typeof(Activity_Proxy))] // unconditional (ACW) -/// [assembly: TypeMap<Java.Lang.Object>("android/widget/TextView", typeof(TextView_Proxy), typeof(TextView))] // trimmable (MCW) -/// [assembly: TypeMapAssociation<Java.Lang.Object>(typeof(MyTextView), typeof(Android_Widget_TextView_Proxy))] // managed → proxy +/// // Assembly-level TypeMap attributes — one per Java peer type. +/// // The anchor type T is Java.Lang.Object in merged mode (Release) or +/// // a per-assembly __TypeMapAnchor in per-assembly mode (Debug): +/// [assembly: TypeMap<T>("android/app/Activity", typeof(Activity_Proxy))] // unconditional (ACW) +/// [assembly: TypeMap<T>("android/widget/TextView", typeof(TextView_Proxy), typeof(TextView))] // trimmable (MCW) +/// [assembly: TypeMapAssociation<T>(typeof(MyTextView), typeof(Android_Widget_TextView_Proxy))] // managed → proxy /// /// // One proxy type per Java peer that needs activation or UCO wrappers: /// public sealed class Activity_Proxy : JavaPeerProxy<Activity>, IAndroidCallableWrapper // IAndroidCallableWrapper for ACWs only @@ -107,6 +109,8 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniEnvTypesRegisterNativesRef; MemberReferenceHandle _readOnlySpanOfJniNativeMethodCtorRef; + EntityHandle _anchorTypeHandle; + /// /// Creates a new emitter. /// @@ -123,7 +127,11 @@ public TypeMapAssemblyEmitter (Version systemRuntimeVersion) /// /// Emits a PE assembly from the given model and writes it to . /// - public void Emit (TypeMapAssemblyData model, Stream stream) + /// + /// When true, uses Java.Lang.Object as the shared anchor type so all assemblies + /// share a single typemap universe. When false, emits a per-assembly __TypeMapAnchor. + /// + public void Emit (TypeMapAssemblyData model, Stream stream, bool useSharedTypemapUniverse = false) { if (model is null) { throw new ArgumentNullException (nameof (model)); @@ -132,17 +140,26 @@ public void Emit (TypeMapAssemblyData model, Stream stream) throw new ArgumentNullException (nameof (stream)); } - EmitCore (model); + EmitCore (model, useSharedTypemapUniverse); _pe.WritePE (stream); } - void EmitCore (TypeMapAssemblyData model) + void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) { _pe.EmitPreamble (model.AssemblyName, model.ModuleName, MetadataHelper.ComputeContentFingerprint (model)); _javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0)); EmitTypeReferences (); + if (useSharedTypemapUniverse) { + // Use Java.Lang.Object as the shared anchor so all assemblies share a single + // typemap universe that can be merged at startup. + _anchorTypeHandle = _pe.Metadata.AddTypeReference (_pe.MonoAndroidRef, + _pe.Metadata.GetOrAddString ("Java.Lang"), + _pe.Metadata.GetOrAddString ("Object")); + } else { + EmitAnchorType (); + } EmitMemberReferences (); // Track wrapper method names → handles for RegisterNatives @@ -212,6 +229,26 @@ void EmitTypeReferences () _readOnlySpanOfJniNativeMethodSpec = MakeGenericTypeSpec_ValueType (_readOnlySpanOpenRef, _jniNativeMethodRef); } + /// + /// Emits an internal __TypeMapAnchor class used as the group type parameter + /// for TypeMap<T> and TypeMapAssociation<T>. Each per-assembly + /// typemap DLL gets its own anchor, creating an isolated typemap universe. + /// + void EmitAnchorType () + { + var metadata = _pe.Metadata; + var objectRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Object")); + + _anchorTypeHandle = metadata.AddTypeDefinition ( + TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Class, + default, + metadata.GetOrAddString ("__TypeMapAnchor"), + objectRef, + MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); + } + void EmitMemberReferences () { _getTypeFromHandleRef = _pe.AddMemberRef (_systemTypeRef, "GetTypeFromHandle", @@ -306,10 +343,8 @@ void EmitTypeMapAttributeCtorRef () var typeMapAttrOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef, metadata.GetOrAddString ("System.Runtime.InteropServices"), metadata.GetOrAddString ("TypeMapAttribute`1")); - var javaLangObjectRef = metadata.AddTypeReference (_pe.MonoAndroidRef, - metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); - var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (typeMapAttrOpenRef, javaLangObjectRef); + var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (typeMapAttrOpenRef, _anchorTypeHandle); // 2-arg: TypeMap(string jniName, Type proxyType) — unconditional _typeMapAttrCtorRef2Arg = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", @@ -337,9 +372,7 @@ void EmitTypeMapAssociationAttributeCtorRef () var typeMapAssociationAttrOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef, metadata.GetOrAddString ("System.Runtime.InteropServices"), metadata.GetOrAddString ("TypeMapAssociationAttribute`1")); - var javaLangObjectRef = metadata.AddTypeReference (_pe.MonoAndroidRef, - metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); - var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (typeMapAssociationAttrOpenRef, javaLangObjectRef); + var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (typeMapAssociationAttrOpenRef, _anchorTypeHandle); _typeMapAssociationAttrCtorRef = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 2cb92963816..939689a43a5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -24,10 +24,13 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// Scanned Java peer types. /// Stream to write the output PE assembly to. /// Assembly name for the generated assembly. - public void Generate (IReadOnlyList peers, Stream stream, string assemblyName) + /// + /// When true, uses Java.Lang.Object as the shared anchor type. When false, emits a per-assembly anchor. + /// + public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false) { var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); - emitter.Emit (model, stream); + emitter.Emit (model, stream, useSharedTypemapUniverse); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 4970d59dc00..5d14490503a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -25,6 +25,7 @@ public TrimmableTypeMapResult Execute ( IReadOnlyList<(string Name, PEReader Reader)> assemblies, Version systemRuntimeVersion, HashSet frameworkAssemblyNames, + bool useSharedTypemapUniverse = false, ManifestConfig? manifestConfig = null, XDocument? manifestTemplate = null) { @@ -41,7 +42,7 @@ public TrimmableTypeMapResult Execute ( RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); PropagateDeferredRegistrationToBaseClasses (allPeers); - var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion); + var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); @@ -112,7 +113,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return (peers, manifestInfo); } - List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion) + List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse) { var peersByAssembly = allPeers.GroupBy (p => p.AssemblyName, StringComparer.Ordinal).OrderBy (g => g.Key, StringComparer.Ordinal); var generatedAssemblies = new List (); @@ -123,14 +124,14 @@ List GenerateTypeMapAssemblies (List allPeers, perAssemblyNames.Add (assemblyName); var peers = group.ToList (); var stream = new MemoryStream (); - generator.Generate (peers, stream, assemblyName); + generator.Generate (peers, stream, assemblyName, useSharedTypemapUniverse); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (assemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (assemblyName, peers.Count); } var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, rootStream); + rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream); rootStream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 8bb98c4a993..52677116363 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -162,10 +162,6 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) ); JniRuntime.SetCurrent (androidRuntime); - if (RuntimeFeature.TrimmableTypeMap) { - TrimmableTypeMap.Initialize (); - } - grefIGCUserPeer_class = args->grefIGCUserPeer; grefGCUserPeerable_class = args->grefGCUserPeerable; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs new file mode 100644 index 00000000000..83925abd7c6 --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs @@ -0,0 +1,44 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Android.Runtime; + +/// +/// Wraps N instances and flattens +/// results across all universes. Debug-only — each assembly has its own +/// universe with an isolated TypeMapLazyDictionary. +/// +sealed class AggregateTypeMap : ITypeMapWithAliasing +{ + readonly SingleUniverseTypeMap[] _universes; + + public AggregateTypeMap (SingleUniverseTypeMap[] universes) + { + ArgumentNullException.ThrowIfNull (universes); + _universes = universes; + } + + public IEnumerable GetTypes (string jniName) + { + foreach (var universe in _universes) { + foreach (var type in universe.GetTypes (jniName)) { + yield return type; + } + } + } + + public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? proxyType) + { + // First-wins: each managed type exists in exactly one assembly + foreach (var universe in _universes) { + if (universe.TryGetProxyType (managedType, out proxyType)) { + return true; + } + } + proxyType = null; + return false; + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs new file mode 100644 index 00000000000..0849741e96d --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs @@ -0,0 +1,29 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Android.Runtime; + +/// +/// Abstraction over the typemap dictionary that handles alias resolution. +/// Both Debug (per-assembly universes) and Release (single merged universe) +/// go through this interface, so doesn't +/// need to know about aliasing mechanics. +/// +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. + /// + IEnumerable GetTypes (string jniName); + + /// + /// Resolves a managed type to its proxy type (the generated type that + /// carries the attribute). + /// + bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? proxyType); +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs new file mode 100644 index 00000000000..95ac2fe4163 --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs @@ -0,0 +1,86 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Java.Interop; + +namespace Microsoft.Android.Runtime; + +/// +/// Wraps a single universe +/// and its proxy type map. Handles +/// alias resolution within that universe. +/// Used in both Debug (one per assembly) and Release (single merged). +/// +sealed class SingleUniverseTypeMap : ITypeMapWithAliasing +{ + readonly IReadOnlyDictionary _typeMap; + readonly IReadOnlyDictionary _proxyTypeMap; + + public SingleUniverseTypeMap (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyTypeMap) + { + ArgumentNullException.ThrowIfNull (typeMap); + ArgumentNullException.ThrowIfNull (proxyTypeMap); + _typeMap = typeMap; + _proxyTypeMap = proxyTypeMap; + } + + public IEnumerable GetTypes (string jniName) + { + if (!_typeMap.TryGetValue (jniName, out var mappedType)) { + yield break; + } + + // Fast path: non-alias entry + if (mappedType.GetCustomAttribute (inherit: false) is not null) { + yield return mappedType; + yield break; + } + + // Slow path: alias holder — follow each alias key + var aliases = mappedType.GetCustomAttribute (inherit: false); + if (aliases is null) { + yield break; + } + + foreach (var key in aliases.Aliases) { + if (_typeMap.TryGetValue (key, out var aliasEntryType) && + aliasEntryType.GetCustomAttribute (inherit: false) is not null) { + yield return aliasEntryType; + } + } + } + + public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? proxyType) + { + if (!_proxyTypeMap.TryGetValue (managedType, out var mappedProxyType)) { + proxyType = null; + return false; + } + + // Fast path: direct proxy + if (mappedProxyType.GetCustomAttribute (inherit: false) is not null) { + proxyType = mappedProxyType; + return true; + } + + // Slow path: alias holder — find the alias whose target type matches + var aliases = mappedProxyType.GetCustomAttribute (inherit: false); + if (aliases is not null) { + foreach (var key in aliases.Aliases) { + if (_typeMap.TryGetValue (key, out var aliasProxyType)) { + var aliasProxy = aliasProxyType.GetCustomAttribute (inherit: false); + if (aliasProxy is not null && TrimmableTypeMap.TargetTypeMatches (managedType, aliasProxy.TargetType)) { + proxyType = aliasProxyType; + return true; + } + } + } + } + + proxyType = null; + return false; + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 5a1a26cf33e..2272fc1d9dc 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -13,7 +13,7 @@ namespace Microsoft.Android.Runtime; /// -/// Central type map for the trimmable typemap path. Owns the TypeMapping dictionary +/// Central type map for the trimmable typemap path. Owns the ITypeMapWithAliasing /// and provides peer creation, invoker resolution, container factories, and native /// method registration. All proxy attribute access is encapsulated here. /// @@ -27,31 +27,57 @@ class TrimmableTypeMap s_instance ?? throw new InvalidOperationException ( "TrimmableTypeMap has not been initialized. Ensure RuntimeFeature.TrimmableTypeMap is enabled and the JNI runtime is initialized."); - readonly IReadOnlyDictionary _typeMap; - readonly IReadOnlyDictionary _proxyTypeMap; + readonly ITypeMapWithAliasing _typeMap; readonly ConcurrentDictionary _proxyCache = new (); readonly ConcurrentDictionary _jniProxyCache = new (StringComparer.Ordinal); - TrimmableTypeMap () + TrimmableTypeMap (ITypeMapWithAliasing typeMap) { - _typeMap = TypeMapping.GetOrCreateExternalTypeMapping (); - _proxyTypeMap = TypeMapping.GetOrCreateProxyTypeMapping (); + _typeMap = typeMap; } /// - /// Initializes the singleton instance and registers the bootstrap JNI native method. - /// Must be called after the JNI runtime is initialized and before any JCW class is loaded. + /// Initializes the singleton with a single merged typemap universe. + /// Called from the startup hook in the generated root assembly (_Microsoft.Android.TypeMaps) + /// when assembly typemaps are merged (Release builds). /// - internal static void Initialize () + internal static void Initialize (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap) { - if (s_instance is not null) - return; + ArgumentNullException.ThrowIfNull (typeMap); + ArgumentNullException.ThrowIfNull (proxyMap); + InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap)); + } + + /// + /// Initializes the singleton with multiple per-assembly typemap universes. + /// Called from the startup hook in the generated root assembly (_Microsoft.Android.TypeMaps) + /// when each assembly has its own typemap universe (Debug builds). + /// + internal static void Initialize (IReadOnlyDictionary[] typeMaps, IReadOnlyDictionary[] proxyMaps) + { + ArgumentNullException.ThrowIfNull (typeMaps); + ArgumentNullException.ThrowIfNull (proxyMaps); + if (typeMaps.Length == 0) { + throw new ArgumentException ("At least one typemap universe must be provided.", nameof (typeMaps)); + } + if (typeMaps.Length != proxyMaps.Length) { + throw new ArgumentException ($"typeMaps.Length ({typeMaps.Length}) must equal proxyMaps.Length ({proxyMaps.Length})."); + } + var universes = new SingleUniverseTypeMap [typeMaps.Length]; + for (int i = 0; i < typeMaps.Length; i++) { + universes [i] = new SingleUniverseTypeMap (typeMaps [i], proxyMaps [i]); + } + InitializeCore (new AggregateTypeMap (universes)); + } + static void InitializeCore (ITypeMapWithAliasing typeMap) + { lock (s_initLock) { - if (s_instance is not null) - return; + if (s_instance is not null) { + throw new InvalidOperationException ("TrimmableTypeMap has already been initialized."); + } - var instance = new TrimmableTypeMap (); + var instance = new TrimmableTypeMap (typeMap); instance.RegisterNatives (); s_instance = instance; } @@ -104,29 +130,11 @@ internal bool TryGetTargetTypes (string jniName, [NotNullWhen (true)] out Type[] JavaPeerProxy[] GetProxiesForJniName (string jniName) { return _jniProxyCache.GetOrAdd (jniName, static (name, self) => { - if (!self._typeMap.TryGetValue (name, out var mappedType)) { - return []; - } - - // Fast path: non-alias entry - var proxy = mappedType.GetCustomAttribute (inherit: false); - if (proxy is not null) { - return [proxy]; - } - - // Slow path: alias holder — resolve each alias key - var aliases = mappedType.GetCustomAttribute (inherit: false); - if (aliases is null) { - return []; - } - var result = new List (); - foreach (var key in aliases.Aliases) { - if (self._typeMap.TryGetValue (key, out var aliasEntryType)) { - var aliasProxy = aliasEntryType.GetCustomAttribute (inherit: false); - if (aliasProxy is not null) { - result.Add (aliasProxy); - } + foreach (var type in self._typeMap.GetTypes (name)) { + var proxy = type.GetCustomAttribute (inherit: false); + if (proxy is not null) { + result.Add (proxy); } } return result.Count > 0 ? result.ToArray () : []; @@ -161,41 +169,15 @@ JavaPeerProxy[] GetProxiesForJniName (string jniName) } var proxy = _proxyCache.GetOrAdd (managedType, static (type, self) => { - if (!self._proxyTypeMap.TryGetValue (type, out var proxyType)) { + if (!self._typeMap.TryGetProxyType (type, out var proxyType)) { return s_noPeerSentinel; } - // Fast path: direct proxy lookup (non-alias types) - var proxy = proxyType.GetCustomAttribute (inherit: false); - if (proxy is not null) { - return proxy; - } - - // Slow path: _proxyTypeMap mapped this type to an alias holder — resolve from aliases - var aliases = proxyType.GetCustomAttribute (inherit: false); - if (aliases is not null) { - return GetProxyFromAliases (self, aliases, type) ?? s_noPeerSentinel; - } - - return s_noPeerSentinel; + return proxyType.GetCustomAttribute (inherit: false) ?? s_noPeerSentinel; }, this); return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } - static JavaPeerProxy? GetProxyFromAliases (TrimmableTypeMap self, JavaPeerAliasesAttribute aliases, Type targetType) - { - foreach (var key in aliases.Aliases) { - if (!self._typeMap.TryGetValue (key, out var aliasProxyType)) { - continue; - } - var aliasProxy = aliasProxyType.GetCustomAttribute (inherit: false); - if (aliasProxy is not null && TargetTypeMatches (targetType, aliasProxy.TargetType)) { - return aliasProxy; - } - } - return null; - } - internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) { jniName = GetProxyForManagedType (managedType)?.JniName; diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 12bb3a01446..55352540d70 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -354,11 +354,14 @@ + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.HotReload.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.HotReload.targets index a1f29e119d7..4156e0db1b5 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.HotReload.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.HotReload.targets @@ -43,10 +43,19 @@ See: https://github.com/dotnet/sdk/pull/52581 + <_ExistingStartupHooks Include="@(RuntimeEnvironmentVariable)" Condition=" '%(Identity)' == 'DOTNET_STARTUP_HOOKS' and '%(Value)' != '$(_AndroidHotReloadAgentAssemblyPath)' " /> - + + + <_ExistingStartupHooks Remove="@(_ExistingStartupHooks)" /> 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 52ce49be5cf..40a2c1519c3 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 @@ -11,6 +11,9 @@ <_TypeMapAssemblyName>_Microsoft.Android.TypeMaps + + true @@ -31,6 +34,28 @@ Condition=" '$(_AndroidRuntime)' == 'CoreCLR' " /> + + + + <_ExistingStartupHooks Include="@(RuntimeEnvironmentVariable)" Condition=" '%(Identity)' == 'DOTNET_STARTUP_HOOKS' " /> + + + + <_ExistingStartupHooks Remove="@(_ExistingStartupHooks)" /> + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 120feb3c59a..f80a14157f1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -129,6 +129,7 @@ public override bool RunTask () assemblies, systemRuntimeVersion, frameworkAssemblyNames, + useSharedTypemapUniverse: !Debug, manifestConfig, manifestTemplate); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 44d357f911b..0acd64e1812 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -11,11 +11,11 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase { - static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, string? assemblyName = null) + static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, bool useSharedTypemapUniverse = false, string? assemblyName = null) { var stream = new MemoryStream (); var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (perAssemblyNames, stream, assemblyName); + generator.Generate (perAssemblyNames, useSharedTypemapUniverse, stream, assemblyName); stream.Position = 0; return stream; } @@ -33,7 +33,7 @@ public void Generate_ProducesValidPEAssembly () [InlineData ("MyRoot", "MyRoot")] public void Generate_AssemblyName_MatchesExpected (string? assemblyName, string expectedName) { - using var stream = GenerateRootAssembly ([], assemblyName); + using var stream = GenerateRootAssembly ([], assemblyName: assemblyName); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); var asmDef = reader.GetAssemblyDefinition (); @@ -54,13 +54,12 @@ public void Generate_ReferencesGenericTypeMapAssemblyTargetAttribute () reader.GetString (t.Name) == "TypeMapAssemblyTargetAttribute`1" && reader.GetString (t.Namespace) == "System.Runtime.InteropServices"); - Assert.Contains (typeRefs, t => - reader.GetString (t.Name) == "Object" && - reader.GetString (t.Namespace) == "Java.Lang"); - var typeDefs = reader.TypeDefinitions .Select (h => reader.GetTypeDefinition (h)) .ToList (); + Assert.Contains (typeDefs, t => + reader.GetString (t.Name) == "__TypeMapAnchor"); + Assert.DoesNotContain (typeDefs, t => reader.GetString (t.Name).Contains ("TypeMapAssemblyTarget")); } @@ -71,8 +70,8 @@ public void Generate_EmptyList_ProducesValidAssemblyWithNoTargetAttributes () using var stream = GenerateRootAssembly ([]); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); - var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - Assert.Empty (asmAttrs); + var targetAttrs = GetTypeMapAssemblyTargetAttributes (reader); + Assert.Empty (targetAttrs); } [Fact] @@ -82,8 +81,8 @@ public void Generate_MultipleTargets_HasCorrectAttributeCount () using var stream = GenerateRootAssembly (targets); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); - var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); - Assert.Equal (3, asmAttrs.Count ()); + var targetAttrs = GetTypeMapAssemblyTargetAttributes (reader); + Assert.Equal (3, targetAttrs.Count); } [Fact] @@ -94,9 +93,10 @@ 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 attrHandle in reader.GetCustomAttributes (EntityHandle.AssemblyDefinition)) { - var attr = reader.GetCustomAttribute (attrHandle); + foreach (var attr in targetAttrs) { var blob = reader.GetBlobReader (attr.Value); // Custom attribute blob: prolog (2 bytes) + SerString value @@ -111,4 +111,130 @@ public void Generate_AttributeBlobValues_MatchTargetNames () Assert.Contains ("_App.TypeMap", attrValues); Assert.Contains ("_Mono.Android.TypeMap", attrValues); } + + static List GetTypeMapAssemblyTargetAttributes (MetadataReader reader) + { + var result = new List (); + foreach (var attrHandle in reader.GetCustomAttributes (EntityHandle.AssemblyDefinition)) { + var attr = reader.GetCustomAttribute (attrHandle); + if (attr.Constructor.Kind == HandleKind.MemberReference) { + var memberRef = reader.GetMemberReference ((MemberReferenceHandle)attr.Constructor); + if (memberRef.Parent.Kind == HandleKind.TypeSpecification) { + // TypeMapAssemblyTargetAttribute is a generic type spec + result.Add (attr); + } + } + } + return result; + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Generate_BothMergeModes_ProduceValidPEAssembly (bool useSharedTypemapUniverse) + { + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], useSharedTypemapUniverse); + using var pe = new PEReader (stream); + Assert.True (pe.HasMetadata); + + var reader = pe.GetMetadataReader (); + + // Both modes should have StartupHook type with Initialize method + var typeDefs = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .ToList (); + Assert.Contains (typeDefs, t => reader.GetString (t.Name) == "StartupHook"); + + // Both modes should have assembly target attributes + var targetAttrs = GetTypeMapAssemblyTargetAttributes (reader); + Assert.Equal (2, targetAttrs.Count); + } + + [Fact] + public void Generate_MergedMode_ReferencesRootAnchorOnly () + { + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], useSharedTypemapUniverse: true); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // In merged mode, the root assembly's __TypeMapAnchor is used. + // The per-assembly anchors should NOT be referenced directly (no assembly refs for per-assembly typemaps). + var asmRefs = reader.AssemblyReferences + .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name)) + .ToList (); + Assert.DoesNotContain ("_App.TypeMap", asmRefs); + Assert.DoesNotContain ("_Mono.Android.TypeMap", asmRefs); + } + + [Fact] + public void Generate_AggregateMode_ReferencesPerAssemblyAnchors () + { + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], useSharedTypemapUniverse: false); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + // In aggregate mode, the root assembly should reference each per-assembly typemap. + var asmRefs = reader.AssemblyReferences + .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name)) + .ToList (); + Assert.Contains ("_App.TypeMap", asmRefs); + Assert.Contains ("_Mono.Android.TypeMap", asmRefs); + } + + [Fact] + public void Generate_MergedMode_HasIgnoresAccessChecksToMonoAndroidOnly () + { + using var stream = GenerateRootAssembly (["_App.TypeMap"], useSharedTypemapUniverse: true); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var accessAttrs = GetIgnoresAccessChecksToValues (reader); + Assert.Contains ("Mono.Android", accessAttrs); + Assert.DoesNotContain ("_App.TypeMap", accessAttrs); + } + + [Fact] + public void Generate_AggregateMode_HasIgnoresAccessChecksToAllAssemblies () + { + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], useSharedTypemapUniverse: false); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var accessAttrs = GetIgnoresAccessChecksToValues (reader); + Assert.Contains ("Mono.Android", accessAttrs); + Assert.Contains ("_App.TypeMap", accessAttrs); + Assert.Contains ("_Mono.Android.TypeMap", accessAttrs); + } + + static List GetIgnoresAccessChecksToValues (MetadataReader reader) + { + var result = new List (); + foreach (var attrHandle in reader.GetCustomAttributes (EntityHandle.AssemblyDefinition)) { + var attr = reader.GetCustomAttribute (attrHandle); + + string? typeName = null; + if (attr.Constructor.Kind == HandleKind.MemberReference) { + var memberRef = reader.GetMemberReference ((MemberReferenceHandle) attr.Constructor); + if (memberRef.Parent.Kind == HandleKind.TypeReference) { + typeName = reader.GetString (reader.GetTypeReference ((TypeReferenceHandle) memberRef.Parent).Name); + } else if (memberRef.Parent.Kind == HandleKind.TypeDefinition) { + typeName = reader.GetString (reader.GetTypeDefinition ((TypeDefinitionHandle) memberRef.Parent).Name); + } + } else if (attr.Constructor.Kind == HandleKind.MethodDefinition) { + var methodDef = reader.GetMethodDefinition ((MethodDefinitionHandle) attr.Constructor); + typeName = reader.GetString (reader.GetTypeDefinition (methodDef.GetDeclaringType ()).Name); + } + + if (typeName != "IgnoresAccessChecksToAttribute") { + continue; + } + var blob = reader.GetBlobReader (attr.Value); + blob.ReadUInt16 (); // prolog + var value = blob.ReadSerializedString (); + if (value is not null) { + result.Add (value); + } + } + return result; + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 8412d665597..973bbc86ffe 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -136,6 +136,7 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet (), + useSharedTypemapUniverse: false, new ManifestConfig ( PackageName: "my.app", AndroidApiLevel: "35",