From 00098c9c7f59bbba6e3886f2a69f74d13ab15a60 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 22 Apr 2026 12:04:21 +0200 Subject: [PATCH 01/13] Implement per-assembly typemap universes for incremental Debug builds Implements #11180: Each assembly gets its own typemap universe via a generated __TypeMapAnchor type (instead of sharing Java.Lang.Object), creating isolated TypeMapLazyDictionary instances per assembly. Runtime layer: - Add ITypeMapWithAliasing interface with GetTypes() and TryGetProxyType() - Add SingleUniverseTypeMap (handles alias resolution within one universe) - Add AggregateTypeMap (flattens across N universes, Debug-only) - Simplify TrimmableTypeMap to consume ITypeMapWithAliasing - Remove TrimmableTypeMap.Initialize() call from JNIEnvInit Generator layer: - Emit __TypeMapAnchor in each per-assembly typemap DLL - Use TypeMap<__TypeMapAnchor> instead of TypeMap - Add isRelease parameter to control universe merging - Root assembly emits StartupHook class with Initialize() IL method - Debug: constructs N SingleUniverseTypeMap + AggregateTypeMap - Release: single SingleUniverseTypeMap - Root assembly emits IgnoresAccessChecksTo for internal type access MSBuild layer: - Enable StartupHookSupport for trimmable typemap builds - Configure DOTNET_STARTUP_HOOKS to point to root typemap assembly - Fix HotReload targets to compose startup hooks instead of replacing Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 294 +++++++++++++++++- .../Generator/TypeMapAssemblyEmitter.cs | 31 +- .../TrimmableTypeMapGenerator.cs | 7 +- .../Android.Runtime/JNIEnvInit.cs | 4 - .../ITypeMapWithAliasing.cs | 146 +++++++++ .../TrimmableTypeMap.cs | 74 +---- .../Microsoft.Android.Sdk.HotReload.targets | 11 +- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 6 + .../Tasks/GenerateTrimmableTypeMap.cs | 1 + .../RootTypeMapAssemblyGeneratorTests.cs | 38 ++- .../TrimmableTypeMapGeneratorTests.cs | 1 + 11 files changed, 512 insertions(+), 101 deletions(-) create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 01f29e322c1..a7f9f377600 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -1,22 +1,50 @@ 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 constructs the +/// appropriate implementation +/// and registers it via . +/// /// /// /// 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 before TrimmableTypeMap.Initialize(): +/// internal static class StartupHook +/// { +/// internal static void Initialize () +/// { +/// // Debug (per-assembly universes): +/// var u0 = new SingleUniverseTypeMap( +/// TypeMapping.GetOrCreateExternalTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>(), +/// TypeMapping.GetOrCreateProxyTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>()); +/// var u1 = new SingleUniverseTypeMap(...); +/// var aggregate = new AggregateTypeMap(new[] { u0, u1 }); +/// TrimmableTypeMap.Initialize(aggregate); +/// +/// // Release (single merged universe): +/// var single = new SingleUniverseTypeMap( +/// TypeMapping.GetOrCreateExternalTypeMapping<__TypeMapAnchor>(), +/// TypeMapping.GetOrCreateProxyTypeMapping<__TypeMapAnchor>()); +/// TrimmableTypeMap.Initialize(single); +/// } +/// } /// /// public sealed class RootTypeMapAssemblyGenerator @@ -35,10 +63,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 for Release (single merged universe), false for Debug (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 isRelease, Stream stream, string? assemblyName = null, string? moduleName = null) { if (perAssemblyTypeMapNames is null) { throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); @@ -53,30 +82,261 @@ 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 + // Emit __TypeMapAnchor type definition (used as group type for root assembly) + var objectRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeRef, + pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object")); + + var 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 (!isRelease) { + accessTargets.AddRange (perAssemblyTypeMapNames); + } + pe.EmitIgnoresAccessChecksToAttribute (accessTargets); + + // Emit StartupHook class with Initialize() method + EmitStartupHook (pe, anchorTypeHandle, perAssemblyTypeMapNames, isRelease); + + pe.WritePE (stream); + } + + static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, TypeDefinitionHandle 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")); + var closedAttrTypeSpec = pe.MakeGenericTypeSpec (openAttrRef, anchorTypeHandle); - // Build TypeSpec for TypeMapAssemblyTargetAttribute - var closedAttrTypeSpec = pe.MakeGenericTypeSpec (openAttrRef, javaLangObjectRef); - - // 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, TypeDefinitionHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool isRelease) + { + 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 singleUniverseTypeMapRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("SingleUniverseTypeMap")); + var aggregateTypeMapRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("AggregateTypeMap")); + var iTypeMapWithAliasingRef = metadata.AddTypeReference (pe.MonoAndroidRef, + metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("ITypeMapWithAliasing")); + 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); + + // SingleUniverseTypeMap..ctor(IReadOnlyDictionary, IReadOnlyDictionary) + var singleCtorRef = AddSingleUniverseCtorRef (pe, singleUniverseTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + + // AggregateTypeMap..ctor(SingleUniverseTypeMap[]) + var aggregateCtorRef = pe.AddMemberRef (aggregateTypeMapRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().SZArray ().Type (singleUniverseTypeMapRef, false))); + + // TrimmableTypeMap.Initialize(ITypeMapWithAliasing) + var initializeRef = pe.AddMemberRef (trimmableTypeMapRef, "Initialize", + sig => sig.MethodSignature ().Parameters (1, + rt => rt.Void (), + p => p.AddParameter ().Type ().Type (iTypeMapWithAliasingRef, 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 (isRelease) { + EmitReleaseInitialize (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, singleCtorRef, initializeRef); + } else { + EmitDebugInitialize (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, singleCtorRef, aggregateCtorRef, singleUniverseTypeMapRef, initializeRef); + } + } + + static void EmitReleaseInitialize (PEAssemblyBuilder pe, TypeDefinitionHandle anchorTypeHandle, + MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, + MemberReferenceHandle singleCtorRef, MemberReferenceHandle initializeRef) + { + var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); + var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); + + pe.EmitBody ("Initialize", + MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, + sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), + encoder => { + // var typeMap = TypeMapping.GetOrCreateExternalTypeMapping<__TypeMapAnchor>(); + encoder.OpCode (ILOpCode.Call); + encoder.Token (getExternalSpec); + // var proxyMap = TypeMapping.GetOrCreateProxyTypeMapping<__TypeMapAnchor>(); + encoder.OpCode (ILOpCode.Call); + encoder.Token (getProxySpec); + // var single = new SingleUniverseTypeMap(typeMap, proxyMap); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (singleCtorRef); + // TrimmableTypeMap.Initialize(single); + encoder.OpCode (ILOpCode.Call); + encoder.Token (initializeRef); + encoder.OpCode (ILOpCode.Ret); + }); + } + + static void EmitDebugInitialize (PEAssemblyBuilder pe, + IReadOnlyList perAssemblyTypeMapNames, + MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, + MemberReferenceHandle singleCtorRef, + MemberReferenceHandle aggregateCtorRef, TypeReferenceHandle singleUniverseTypeMapRef, + MemberReferenceHandle initializeRef) + { + 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.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, + sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), + encoder => { + // var universes = new SingleUniverseTypeMap[N]; + encoder.LoadConstantI4 (count); + encoder.OpCode (ILOpCode.Newarr); + encoder.Token (singleUniverseTypeMapRef); + + for (int i = 0; i < count; i++) { + encoder.OpCode (ILOpCode.Dup); + encoder.LoadConstantI4 (i); + // TypeMapping.GetOrCreateExternalTypeMapping<_X.TypeMap.__TypeMapAnchor>() + encoder.OpCode (ILOpCode.Call); + encoder.Token (getExternalSpecs [i]); + // TypeMapping.GetOrCreateProxyTypeMapping<_X.TypeMap.__TypeMapAnchor>() + encoder.OpCode (ILOpCode.Call); + encoder.Token (getProxySpecs [i]); + // new SingleUniverseTypeMap(typeMap, proxyMap) + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (singleCtorRef); + encoder.OpCode (ILOpCode.Stelem_ref); + } + + // var aggregate = new AggregateTypeMap(universes); + encoder.OpCode (ILOpCode.Newobj); + encoder.Token (aggregateCtorRef); + // TrimmableTypeMap.Initialize(aggregate); + encoder.OpCode (ILOpCode.Call); + encoder.Token (initializeRef); + encoder.OpCode (ILOpCode.Ret); + }); + } + + /// + /// 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 SingleUniverseTypeMap..ctor(IReadOnlyDictionary<string, Type>, IReadOnlyDictionary<Type, Type>). + /// + static MemberReferenceHandle AddSingleUniverseCtorRef (PEAssemblyBuilder pe, TypeReferenceHandle singleUniverseTypeMapRef, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + { + var blob = new BlobBuilder (64); + // Instance method signature: HASTHIS | DEFAULT + blob.WriteByte (0x20); // IMAGE_CEE_CS_CALLCONV_HASTHIS + blob.WriteCompressedInteger (2); // parameter count + blob.WriteByte (0x01); // return type: void + // Param 1: IReadOnlyDictionary + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + // Param 2: IReadOnlyDictionary + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + return pe.Metadata.AddMemberReference (singleUniverseTypeMapRef, + pe.Metadata.GetOrAddString (".ctor"), 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..21317c54deb 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -107,6 +107,8 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniEnvTypesRegisterNativesRef; MemberReferenceHandle _readOnlySpanOfJniNativeMethodCtorRef; + TypeDefinitionHandle _anchorTypeHandle; + /// /// Creates a new emitter. /// @@ -143,6 +145,7 @@ void EmitCore (TypeMapAssemblyData model) _javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0)); EmitTypeReferences (); + EmitAnchorType (); EmitMemberReferences (); // Track wrapper method names → handles for RegisterNatives @@ -212,6 +215,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 +329,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 +358,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/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 4970d59dc00..670ad0c9943 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 isRelease = 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, isRelease); 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 isRelease) { var peersByAssembly = allPeers.GroupBy (p => p.AssemblyName, StringComparer.Ordinal).OrderBy (g => g.Key, StringComparer.Ordinal); var generatedAssemblies = new List (); @@ -130,7 +131,7 @@ List GenerateTypeMapAssemblies (List allPeers, } var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, rootStream); + rootGenerator.Generate (perAssemblyNames, isRelease, 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/ITypeMapWithAliasing.cs b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs new file mode 100644 index 00000000000..fbe7538c2af --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs @@ -0,0 +1,146 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.InteropServices; +using Java.Interop; + +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); +} + +/// +/// 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; + } +} + +/// +/// 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/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 5a1a26cf33e..a9e3d19b9ad 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,23 +27,23 @@ 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. + /// Called from the startup hook in the generated root assembly (_Microsoft.Android.TypeMaps). /// - internal static void Initialize () + internal static void Initialize (ITypeMapWithAliasing typeMap) { + ArgumentNullException.ThrowIfNull (typeMap); + if (s_instance is not null) return; @@ -51,7 +51,7 @@ internal static void Initialize () if (s_instance is not null) return; - var instance = new TrimmableTypeMap (); + var instance = new TrimmableTypeMap (typeMap); instance.RegisterNatives (); s_instance = instance; } @@ -104,29 +104,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 +143,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/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..95f80596be8 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..4f580d9a161 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 @@ -26,6 +29,9 @@ Value="true" Trim="true" /> + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 120feb3c59a..8fd5208765f 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, + isRelease: !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..afdd4d12908 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -15,7 +15,7 @@ static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames { var stream = new MemoryStream (); var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (perAssemblyNames, stream, assemblyName); + generator.Generate (perAssemblyNames, isRelease: false, stream, assemblyName); stream.Position = 0; return stream; } @@ -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,20 @@ 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; + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 8412d665597..ef1821ac643 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 (), + isRelease: false, new ManifestConfig ( PackageName: "my.app", AndroidApiLevel: "35", From 362808862e35847c4a5b1786336409267044e32e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 22 Apr 2026 13:16:09 +0200 Subject: [PATCH 02/13] Address review feedback: rename params, BCL-type Initialize overloads, startup hook fixes, tests - Rename isRelease -> mergeAssemblyTypeMaps throughout generator and MSBuild task - Rename EmitReleaseInitialize -> EmitInitializeWithSingleTypeMap - Rename EmitDebugInitialize -> EmitInitializeWithAggregateTypeMap - Change TrimmableTypeMap.Initialize to two overloads with BCL types only: - Initialize(IReadOnlyDictionary, IReadOnlyDictionary) - Initialize(IReadOnlyDictionary[], IReadOnlyDictionary[]) - Generated assembly no longer references SingleUniverseTypeMap/AggregateTypeMap - Add InitializeCore that throws InvalidOperationException on re-initialization - Force-enable StartupHookSupport unconditionally in targets - Remove MonoVM STARTUP_HOOKS line (trimmable typemaps don't work with Mono) - Add tests for both mergeAssemblyTypeMaps modes (true/false) - Add tests for IgnoresAccessChecksTo attribute in both modes - Fix locals encoding for aggregate mode (pass type refs to encodeLocals lambda) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 186 ++++++++++-------- .../TrimmableTypeMapGenerator.cs | 8 +- .../TrimmableTypeMap.cs | 41 +++- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 5 +- .../Tasks/GenerateTrimmableTypeMap.cs | 2 +- .../RootTypeMapAssemblyGeneratorTests.cs | 116 ++++++++++- .../TrimmableTypeMapGeneratorTests.cs | 2 +- 7 files changed, 263 insertions(+), 97 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index a7f9f377600..926d35b00ff 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -11,9 +11,9 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// 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 constructs the -/// appropriate implementation -/// and registers it via . +/// Emits a StartupHook class whose Initialize() method calls +/// with the appropriate +/// type mapping dictionaries. /// /// /// @@ -25,24 +25,26 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// [assembly: TypeMapAssemblyTarget<__TypeMapAnchor>("_Mono.Android.TypeMap")] /// [assembly: TypeMapAssemblyTarget<__TypeMapAnchor>("_MyApp.TypeMap")] /// -/// // Startup hook — called by DOTNET_STARTUP_HOOKS before TrimmableTypeMap.Initialize(): +/// // Startup hook — called by DOTNET_STARTUP_HOOKS: /// internal static class StartupHook /// { /// internal static void Initialize () /// { -/// // Debug (per-assembly universes): -/// var u0 = new SingleUniverseTypeMap( -/// TypeMapping.GetOrCreateExternalTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>(), -/// TypeMapping.GetOrCreateProxyTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>()); -/// var u1 = new SingleUniverseTypeMap(...); -/// var aggregate = new AggregateTypeMap(new[] { u0, u1 }); -/// TrimmableTypeMap.Initialize(aggregate); -/// -/// // Release (single merged universe): -/// var single = new SingleUniverseTypeMap( +/// // Merged (single universe): +/// TrimmableTypeMap.Initialize( /// TypeMapping.GetOrCreateExternalTypeMapping<__TypeMapAnchor>(), /// TypeMapping.GetOrCreateProxyTypeMapping<__TypeMapAnchor>()); -/// TrimmableTypeMap.Initialize(single); +/// +/// // Per-assembly (aggregate universes): +/// 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); /// } /// } /// @@ -63,11 +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 for Release (single merged universe), false for Debug (per-assembly universes). + /// 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, bool isRelease, Stream stream, string? assemblyName = null, string? moduleName = null) + public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool mergeAssemblyTypeMaps, Stream stream, string? assemblyName = null, string? moduleName = null) { if (perAssemblyTypeMapNames is null) { throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); @@ -101,13 +103,13 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool isRele // internal types (SingleUniverseTypeMap, AggregateTypeMap, TrimmableTypeMap in Mono.Android, // and __TypeMapAnchor in each per-assembly typemap DLL). var accessTargets = new List { "Mono.Android" }; - if (!isRelease) { + if (!mergeAssemblyTypeMaps) { accessTargets.AddRange (perAssemblyTypeMapNames); } pe.EmitIgnoresAccessChecksToAttribute (accessTargets); // Emit StartupHook class with Initialize() method - EmitStartupHook (pe, anchorTypeHandle, perAssemblyTypeMapNames, isRelease); + EmitStartupHook (pe, anchorTypeHandle, perAssemblyTypeMapNames, mergeAssemblyTypeMaps); pe.WritePE (stream); } @@ -131,7 +133,7 @@ static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, TypeDefinitionHa } } - static void EmitStartupHook (PEAssemblyBuilder pe, TypeDefinitionHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool isRelease) + static void EmitStartupHook (PEAssemblyBuilder pe, TypeDefinitionHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool mergeAssemblyTypeMaps) { var metadata = pe.Metadata; @@ -141,12 +143,6 @@ static void EmitStartupHook (PEAssemblyBuilder pe, TypeDefinitionHandle anchorTy var systemTypeRef = metadata.AddTypeReference (pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Type")); - var singleUniverseTypeMapRef = metadata.AddTypeReference (pe.MonoAndroidRef, - metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("SingleUniverseTypeMap")); - var aggregateTypeMapRef = metadata.AddTypeReference (pe.MonoAndroidRef, - metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("AggregateTypeMap")); - var iTypeMapWithAliasingRef = metadata.AddTypeReference (pe.MonoAndroidRef, - metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("ITypeMapWithAliasing")); var trimmableTypeMapRef = metadata.AddTypeReference (pe.MonoAndroidRef, metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("TrimmableTypeMap")); @@ -162,21 +158,6 @@ static void EmitStartupHook (PEAssemblyBuilder pe, TypeDefinitionHandle anchorTy var getProxyMemberRef = AddTypeMappingMethodRef (pe, typeMappingRef, "GetOrCreateProxyTypeMapping", iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - // SingleUniverseTypeMap..ctor(IReadOnlyDictionary, IReadOnlyDictionary) - var singleCtorRef = AddSingleUniverseCtorRef (pe, singleUniverseTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - - // AggregateTypeMap..ctor(SingleUniverseTypeMap[]) - var aggregateCtorRef = pe.AddMemberRef (aggregateTypeMapRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, - rt => rt.Void (), - p => p.AddParameter ().Type ().SZArray ().Type (singleUniverseTypeMapRef, false))); - - // TrimmableTypeMap.Initialize(ITypeMapWithAliasing) - var initializeRef = pe.AddMemberRef (trimmableTypeMapRef, "Initialize", - sig => sig.MethodSignature ().Parameters (1, - rt => rt.Void (), - p => p.AddParameter ().Type ().Type (iTypeMapWithAliasingRef, false))); - // Define the StartupHook type metadata.AddTypeDefinition ( TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Abstract | TypeAttributes.Class, @@ -187,16 +168,22 @@ static void EmitStartupHook (PEAssemblyBuilder pe, TypeDefinitionHandle anchorTy MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); - if (isRelease) { - EmitReleaseInitialize (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, singleCtorRef, initializeRef); + if (mergeAssemblyTypeMaps) { + // TrimmableTypeMap.Initialize(IReadOnlyDictionary, IReadOnlyDictionary) + var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); } else { - EmitDebugInitialize (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, singleCtorRef, aggregateCtorRef, singleUniverseTypeMapRef, initializeRef); + // 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 EmitReleaseInitialize (PEAssemblyBuilder pe, TypeDefinitionHandle anchorTypeHandle, + static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, TypeDefinitionHandle anchorTypeHandle, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, - MemberReferenceHandle singleCtorRef, MemberReferenceHandle initializeRef) + MemberReferenceHandle initializeRef) { var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); @@ -205,33 +192,30 @@ static void EmitReleaseInitialize (PEAssemblyBuilder pe, TypeDefinitionHandle an MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { - // var typeMap = TypeMapping.GetOrCreateExternalTypeMapping<__TypeMapAnchor>(); + // TypeMapping.GetOrCreateExternalTypeMapping<__TypeMapAnchor>() encoder.OpCode (ILOpCode.Call); encoder.Token (getExternalSpec); - // var proxyMap = TypeMapping.GetOrCreateProxyTypeMapping<__TypeMapAnchor>(); + // TypeMapping.GetOrCreateProxyTypeMapping<__TypeMapAnchor>() encoder.OpCode (ILOpCode.Call); encoder.Token (getProxySpec); - // var single = new SingleUniverseTypeMap(typeMap, proxyMap); - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (singleCtorRef); - // TrimmableTypeMap.Initialize(single); + // TrimmableTypeMap.Initialize(typeMap, proxyMap) encoder.OpCode (ILOpCode.Call); encoder.Token (initializeRef); encoder.OpCode (ILOpCode.Ret); }); } - static void EmitDebugInitialize (PEAssemblyBuilder pe, + static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, IReadOnlyList perAssemblyTypeMapNames, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, - MemberReferenceHandle singleCtorRef, - MemberReferenceHandle aggregateCtorRef, TypeReferenceHandle singleUniverseTypeMapRef, - MemberReferenceHandle initializeRef) + MemberReferenceHandle initializeRef, + TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { var count = perAssemblyTypeMapNames.Count; - var getExternalSpecs = new EntityHandle[count]; - var getProxySpecs = new EntityHandle[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, @@ -244,33 +228,51 @@ static void EmitDebugInitialize (PEAssemblyBuilder pe, MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { - // var universes = new SingleUniverseTypeMap[N]; + // var typeMaps = new IReadOnlyDictionary[N]; encoder.LoadConstantI4 (count); encoder.OpCode (ILOpCode.Newarr); - encoder.Token (singleUniverseTypeMapRef); + encoder.Token (externalDictTypeSpec); + encoder.OpCode (ILOpCode.Stloc_0); for (int i = 0; i < count; i++) { - encoder.OpCode (ILOpCode.Dup); + encoder.OpCode (ILOpCode.Ldloc_0); encoder.LoadConstantI4 (i); - // TypeMapping.GetOrCreateExternalTypeMapping<_X.TypeMap.__TypeMapAnchor>() encoder.OpCode (ILOpCode.Call); encoder.Token (getExternalSpecs [i]); - // TypeMapping.GetOrCreateProxyTypeMapping<_X.TypeMap.__TypeMapAnchor>() + 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]); - // new SingleUniverseTypeMap(typeMap, proxyMap) - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (singleCtorRef); encoder.OpCode (ILOpCode.Stelem_ref); } - // var aggregate = new AggregateTypeMap(universes); - encoder.OpCode (ILOpCode.Newobj); - encoder.Token (aggregateCtorRef); - // TrimmableTypeMap.Initialize(aggregate); + // 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); }); } @@ -306,22 +308,50 @@ static MemberReferenceHandle AddTypeMappingMethodRef (PEAssemblyBuilder pe, Type } /// - /// Creates a MemberRef for SingleUniverseTypeMap..ctor(IReadOnlyDictionary<string, Type>, IReadOnlyDictionary<Type, Type>). + /// Creates a MemberRef for TrimmableTypeMap.Initialize(IReadOnlyDictionary<string, Type>, IReadOnlyDictionary<Type, Type>). /// - static MemberReferenceHandle AddSingleUniverseCtorRef (PEAssemblyBuilder pe, TypeReferenceHandle singleUniverseTypeMapRef, + static MemberReferenceHandle AddInitializeSingleRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { var blob = new BlobBuilder (64); - // Instance method signature: HASTHIS | DEFAULT - blob.WriteByte (0x20); // IMAGE_CEE_CS_CALLCONV_HASTHIS + blob.WriteByte (0x00); // DEFAULT (static) blob.WriteCompressedInteger (2); // parameter count blob.WriteByte (0x01); // return type: void - // Param 1: IReadOnlyDictionary EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - // Param 2: IReadOnlyDictionary EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - return pe.Metadata.AddMemberReference (singleUniverseTypeMapRef, - pe.Metadata.GetOrAddString (".ctor"), pe.Metadata.GetOrAddBlob (blob)); + 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) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 670ad0c9943..bc6b8572979 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -25,7 +25,7 @@ public TrimmableTypeMapResult Execute ( IReadOnlyList<(string Name, PEReader Reader)> assemblies, Version systemRuntimeVersion, HashSet frameworkAssemblyNames, - bool isRelease = false, + bool mergeAssemblyTypeMaps = false, ManifestConfig? manifestConfig = null, XDocument? manifestTemplate = null) { @@ -42,7 +42,7 @@ public TrimmableTypeMapResult Execute ( RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); PropagateDeferredRegistrationToBaseClasses (allPeers); - var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, isRelease); + var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, mergeAssemblyTypeMaps); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); @@ -113,7 +113,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return (peers, manifestInfo); } - List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool isRelease) + List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool mergeAssemblyTypeMaps) { var peersByAssembly = allPeers.GroupBy (p => p.AssemblyName, StringComparer.Ordinal).OrderBy (g => g.Key, StringComparer.Ordinal); var generatedAssemblies = new List (); @@ -131,7 +131,7 @@ List GenerateTypeMapAssemblies (List allPeers, } var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, isRelease, rootStream); + rootGenerator.Generate (perAssemblyNames, mergeAssemblyTypeMaps, rootStream); rootStream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index a9e3d19b9ad..9c957562aca 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -37,19 +37,46 @@ class TrimmableTypeMap } /// - /// Initializes the singleton instance and registers the bootstrap JNI native method. - /// Called from the startup hook in the generated root assembly (_Microsoft.Android.TypeMaps). + /// 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 (ITypeMapWithAliasing typeMap) + internal static void Initialize (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap) { 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 != 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)); + } - if (s_instance is not null) - return; + static void InitializeCore (ITypeMapWithAliasing typeMap) + { + if (s_instance is not null) { + throw new InvalidOperationException ("TrimmableTypeMap has already been initialized."); + } 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 (typeMap); instance.RegisterNatives (); 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 4f580d9a161..cd2c296fa61 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 @@ -12,8 +12,8 @@ <_TypeMapAssemblyName>_Microsoft.Android.TypeMaps - true + that constructs the type mapping dictionaries and calls TrimmableTypeMap.Initialize(). --> + true @@ -31,7 +31,6 @@ Value="$(_TypeMapAssemblyName)" /> - diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 8fd5208765f..fd0012b2644 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -129,7 +129,7 @@ public override bool RunTask () assemblies, systemRuntimeVersion, frameworkAssemblyNames, - isRelease: !Debug, + mergeAssemblyTypeMaps: !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 afdd4d12908..c1c895df2a0 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 mergeAssemblyTypeMaps = false, string? assemblyName = null) { var stream = new MemoryStream (); var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (perAssemblyNames, isRelease: false, stream, assemblyName); + generator.Generate (perAssemblyNames, mergeAssemblyTypeMaps, 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 (); @@ -127,4 +127,114 @@ static List GetTypeMapAssemblyTargetAttributes (MetadataReader } return result; } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Generate_BothMergeModes_ProduceValidPEAssembly (bool mergeAssemblyTypeMaps) + { + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], mergeAssemblyTypeMaps); + 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"], mergeAssemblyTypeMaps: 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"], mergeAssemblyTypeMaps: 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"], mergeAssemblyTypeMaps: 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"], mergeAssemblyTypeMaps: 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 ef1821ac643..28293cb6c83 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -136,7 +136,7 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet (), - isRelease: false, + mergeAssemblyTypeMaps: false, new ManifestConfig ( PackageName: "my.app", AndroidApiLevel: "35", From 75998dd09b2ca4b598dd894d0688e258518b471b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 22 Apr 2026 13:19:50 +0200 Subject: [PATCH 03/13] Fix anchor type mismatch: use Java.Lang.Object in merged mode In merged mode (Release), all per-assembly typemap DLLs must use the same anchor type so TypeMapping.GetOrCreateExternalTypeMapping() finds all entries across all assemblies. Previously each DLL emitted its own __TypeMapAnchor, causing the root assembly's GetOrCreate call (using its own anchor) to find nothing. Fix: when mergeAssemblyTypeMaps=true, per-assembly DLLs reference Java.Lang.Object as the anchor type instead of emitting __TypeMapAnchor. The root assembly also references Java.Lang.Object in merged mode. In aggregate mode (Debug), each DLL keeps its own __TypeMapAnchor for isolated per-assembly universes. The root references each per-assembly anchor individually. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 37 ++++++++++++------- .../Generator/TypeMapAssemblyEmitter.cs | 32 +++++++++++----- .../Generator/TypeMapAssemblyGenerator.cs | 7 +++- .../TrimmableTypeMapGenerator.cs | 2 +- 4 files changed, 52 insertions(+), 26 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 926d35b00ff..f1a5f8c6950 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -84,17 +84,26 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool mergeA var pe = new PEAssemblyBuilder (_systemRuntimeVersion); pe.EmitPreamble (assemblyName, moduleName); - // Emit __TypeMapAnchor type definition (used as group type for root assembly) - var objectRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeRef, - pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object")); - - var 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)); + EntityHandle anchorTypeHandle; + if (mergeAssemblyTypeMaps) { + // 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); @@ -114,7 +123,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool mergeA pe.WritePE (stream); } - static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, TypeDefinitionHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames) + static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames) { var openAttrRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeInteropServicesRef, pe.Metadata.GetOrAddString ("System.Runtime.InteropServices"), @@ -133,7 +142,7 @@ static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, TypeDefinitionHa } } - static void EmitStartupHook (PEAssemblyBuilder pe, TypeDefinitionHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool mergeAssemblyTypeMaps) + static void EmitStartupHook (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool mergeAssemblyTypeMaps) { var metadata = pe.Metadata; @@ -181,7 +190,7 @@ static void EmitStartupHook (PEAssemblyBuilder pe, TypeDefinitionHandle anchorTy } } - static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, TypeDefinitionHandle anchorTypeHandle, + static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, MemberReferenceHandle initializeRef) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 21317c54deb..1be6f614a0f 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,7 +109,7 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniEnvTypesRegisterNativesRef; MemberReferenceHandle _readOnlySpanOfJniNativeMethodCtorRef; - TypeDefinitionHandle _anchorTypeHandle; + EntityHandle _anchorTypeHandle; /// /// Creates a new emitter. @@ -125,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 mergeAssemblyTypeMaps = false) { if (model is null) { throw new ArgumentNullException (nameof (model)); @@ -134,18 +140,26 @@ public void Emit (TypeMapAssemblyData model, Stream stream) throw new ArgumentNullException (nameof (stream)); } - EmitCore (model); + EmitCore (model, mergeAssemblyTypeMaps); _pe.WritePE (stream); } - void EmitCore (TypeMapAssemblyData model) + void EmitCore (TypeMapAssemblyData model, bool mergeAssemblyTypeMaps) { _pe.EmitPreamble (model.AssemblyName, model.ModuleName, MetadataHelper.ComputeContentFingerprint (model)); _javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0)); EmitTypeReferences (); - EmitAnchorType (); + if (mergeAssemblyTypeMaps) { + // 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 diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 2cb92963816..d88c2e6e414 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 mergeAssemblyTypeMaps = false) { var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); - emitter.Emit (model, stream); + emitter.Emit (model, stream, mergeAssemblyTypeMaps); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index bc6b8572979..7d82fc8708c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -124,7 +124,7 @@ 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, mergeAssemblyTypeMaps); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (assemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (assemblyName, peers.Count); From 50cb15e29d755bffdaf98863d3ed78ca4a33937c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 22 Apr 2026 13:30:09 +0200 Subject: [PATCH 04/13] Rename mergeAssemblyTypeMaps -> useSharedTypemapUniverse Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 14 +++++++------- .../Generator/TypeMapAssemblyEmitter.cs | 10 +++++----- .../Generator/TypeMapAssemblyGenerator.cs | 6 +++--- .../TrimmableTypeMapGenerator.cs | 10 +++++----- .../Tasks/GenerateTrimmableTypeMap.cs | 2 +- .../RootTypeMapAssemblyGeneratorTests.cs | 16 ++++++++-------- .../Generator/TrimmableTypeMapGeneratorTests.cs | 2 +- 7 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index f1a5f8c6950..fba57362e90 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -65,11 +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. + /// 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, bool mergeAssemblyTypeMaps, 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)); @@ -85,7 +85,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool mergeA pe.EmitPreamble (assemblyName, moduleName); EntityHandle anchorTypeHandle; - if (mergeAssemblyTypeMaps) { + 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, @@ -112,13 +112,13 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool mergeA // internal types (SingleUniverseTypeMap, AggregateTypeMap, TrimmableTypeMap in Mono.Android, // and __TypeMapAnchor in each per-assembly typemap DLL). var accessTargets = new List { "Mono.Android" }; - if (!mergeAssemblyTypeMaps) { + if (!useSharedTypemapUniverse) { accessTargets.AddRange (perAssemblyTypeMapNames); } pe.EmitIgnoresAccessChecksToAttribute (accessTargets); // Emit StartupHook class with Initialize() method - EmitStartupHook (pe, anchorTypeHandle, perAssemblyTypeMapNames, mergeAssemblyTypeMaps); + EmitStartupHook (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse); pe.WritePE (stream); } @@ -142,7 +142,7 @@ static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anc } } - static void EmitStartupHook (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool mergeAssemblyTypeMaps) + static void EmitStartupHook (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse) { var metadata = pe.Metadata; @@ -177,7 +177,7 @@ static void EmitStartupHook (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); - if (mergeAssemblyTypeMaps) { + if (useSharedTypemapUniverse) { // TrimmableTypeMap.Initialize(IReadOnlyDictionary, IReadOnlyDictionary) var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 1be6f614a0f..59c01a00bab 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -127,11 +127,11 @@ public TypeMapAssemblyEmitter (Version systemRuntimeVersion) /// /// Emits a PE assembly from the given model and writes it to . /// - /// + /// /// 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 mergeAssemblyTypeMaps = false) + public void Emit (TypeMapAssemblyData model, Stream stream, bool useSharedTypemapUniverse = false) { if (model is null) { throw new ArgumentNullException (nameof (model)); @@ -140,18 +140,18 @@ public void Emit (TypeMapAssemblyData model, Stream stream, bool mergeAssemblyTy throw new ArgumentNullException (nameof (stream)); } - EmitCore (model, mergeAssemblyTypeMaps); + EmitCore (model, useSharedTypemapUniverse); _pe.WritePE (stream); } - void EmitCore (TypeMapAssemblyData model, bool mergeAssemblyTypeMaps) + 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 (mergeAssemblyTypeMaps) { + 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, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index d88c2e6e414..939689a43a5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -24,13 +24,13 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// Scanned Java peer types. /// Stream to write the output PE assembly to. /// Assembly name for the generated assembly. - /// + /// /// 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 mergeAssemblyTypeMaps = false) + 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, mergeAssemblyTypeMaps); + emitter.Emit (model, stream, useSharedTypemapUniverse); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 7d82fc8708c..5d14490503a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -25,7 +25,7 @@ public TrimmableTypeMapResult Execute ( IReadOnlyList<(string Name, PEReader Reader)> assemblies, Version systemRuntimeVersion, HashSet frameworkAssemblyNames, - bool mergeAssemblyTypeMaps = false, + bool useSharedTypemapUniverse = false, ManifestConfig? manifestConfig = null, XDocument? manifestTemplate = null) { @@ -42,7 +42,7 @@ public TrimmableTypeMapResult Execute ( RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); PropagateDeferredRegistrationToBaseClasses (allPeers); - var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, mergeAssemblyTypeMaps); + var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); @@ -113,7 +113,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return (peers, manifestInfo); } - List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool mergeAssemblyTypeMaps) + 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 (); @@ -124,14 +124,14 @@ List GenerateTypeMapAssemblies (List allPeers, perAssemblyNames.Add (assemblyName); var peers = group.ToList (); var stream = new MemoryStream (); - generator.Generate (peers, stream, assemblyName, mergeAssemblyTypeMaps); + 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, mergeAssemblyTypeMaps, 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/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index fd0012b2644..f80a14157f1 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -129,7 +129,7 @@ public override bool RunTask () assemblies, systemRuntimeVersion, frameworkAssemblyNames, - mergeAssemblyTypeMaps: !Debug, + 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 c1c895df2a0..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, bool mergeAssemblyTypeMaps = false, 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, mergeAssemblyTypeMaps, stream, assemblyName); + generator.Generate (perAssemblyNames, useSharedTypemapUniverse, stream, assemblyName); stream.Position = 0; return stream; } @@ -131,9 +131,9 @@ static List GetTypeMapAssemblyTargetAttributes (MetadataReader [Theory] [InlineData (true)] [InlineData (false)] - public void Generate_BothMergeModes_ProduceValidPEAssembly (bool mergeAssemblyTypeMaps) + public void Generate_BothMergeModes_ProduceValidPEAssembly (bool useSharedTypemapUniverse) { - using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], mergeAssemblyTypeMaps); + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], useSharedTypemapUniverse); using var pe = new PEReader (stream); Assert.True (pe.HasMetadata); @@ -153,7 +153,7 @@ public void Generate_BothMergeModes_ProduceValidPEAssembly (bool mergeAssemblyTy [Fact] public void Generate_MergedMode_ReferencesRootAnchorOnly () { - using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], mergeAssemblyTypeMaps: true); + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], useSharedTypemapUniverse: true); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); @@ -169,7 +169,7 @@ public void Generate_MergedMode_ReferencesRootAnchorOnly () [Fact] public void Generate_AggregateMode_ReferencesPerAssemblyAnchors () { - using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], mergeAssemblyTypeMaps: false); + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], useSharedTypemapUniverse: false); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); @@ -184,7 +184,7 @@ public void Generate_AggregateMode_ReferencesPerAssemblyAnchors () [Fact] public void Generate_MergedMode_HasIgnoresAccessChecksToMonoAndroidOnly () { - using var stream = GenerateRootAssembly (["_App.TypeMap"], mergeAssemblyTypeMaps: true); + using var stream = GenerateRootAssembly (["_App.TypeMap"], useSharedTypemapUniverse: true); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); @@ -196,7 +196,7 @@ public void Generate_MergedMode_HasIgnoresAccessChecksToMonoAndroidOnly () [Fact] public void Generate_AggregateMode_HasIgnoresAccessChecksToAllAssemblies () { - using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], mergeAssemblyTypeMaps: false); + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], useSharedTypemapUniverse: false); using var pe = new PEReader (stream); var reader = pe.GetMetadataReader (); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 28293cb6c83..973bbc86ffe 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -136,7 +136,7 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet (), - mergeAssemblyTypeMaps: false, + useSharedTypemapUniverse: false, new ManifestConfig ( PackageName: "my.app", AndroidApiLevel: "35", From a25b04a0c5ddbf828cdac480f21dc713c05c238e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 22 Apr 2026 13:37:10 +0200 Subject: [PATCH 05/13] Consolidate Initialize to single array overload, update doc comments - Merge two TrimmableTypeMap.Initialize overloads into one that takes arrays and optimizes for single-element case (uses SingleUniverseTypeMap) - Add length-zero validation - Unify IL emission: both modes now use same EmitInitializeBody (always builds arrays), removing EmitInitializeWithSingleTypeMap - Remove AddInitializeSingleRef (only AddInitializeRef remains) - Update doc comments: Option A (shared universe) / Option B (per-assembly) - Fix doc to show Java.Lang.Object as anchor for shared universe mode Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 98 ++++++------------- .../TrimmableTypeMap.cs | 28 +++--- 2 files changed, 45 insertions(+), 81 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index fba57362e90..4ea198865a0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -30,12 +30,12 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// { /// internal static void Initialize () /// { -/// // Merged (single universe): +/// // Option A: Shared universe (Release): /// TrimmableTypeMap.Initialize( -/// TypeMapping.GetOrCreateExternalTypeMapping<__TypeMapAnchor>(), -/// TypeMapping.GetOrCreateProxyTypeMapping<__TypeMapAnchor>()); +/// new[] { TypeMapping.GetOrCreateExternalTypeMapping<Java.Lang.Object>() }, +/// new[] { TypeMapping.GetOrCreateProxyTypeMapping<Java.Lang.Object>() }); /// -/// // Per-assembly (aggregate universes): +/// // Option B: Per-assembly universes, aggregated (Debug): /// var typeMaps = new IReadOnlyDictionary<string, Type>[] { /// TypeMapping.GetOrCreateExternalTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>(), /// TypeMapping.GetOrCreateExternalTypeMapping<_MyApp_TypeMap.__TypeMapAnchor>(), @@ -177,61 +177,43 @@ static void EmitStartupHook (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); + // TrimmableTypeMap.Initialize(IReadOnlyDictionary[], IReadOnlyDictionary[]) + // Both modes use the same overload — shared universe passes 1-element arrays. + var initializeRef = AddInitializeRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + + // Resolve anchor type(s) to method specs + EntityHandle[] getExternalSpecs; + EntityHandle[] getProxySpecs; if (useSharedTypemapUniverse) { - // TrimmableTypeMap.Initialize(IReadOnlyDictionary, IReadOnlyDictionary) - var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); + // Single shared universe — 1-element arrays using the shared anchor (Java.Lang.Object) + getExternalSpecs = new EntityHandle[] { MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle) }; + getProxySpecs = new EntityHandle[] { MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle) }; } 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); + // Per-assembly universes — one element per assembly, each with its own __TypeMapAnchor + var count = perAssemblyTypeMapNames.Count; + getExternalSpecs = new EntityHandle [count]; + 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); + } } - } - - 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.Assembly | 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); - }); + EmitInitializeBody (pe, getExternalSpecs, getProxySpecs, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef); } - static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, - IReadOnlyList perAssemblyTypeMapNames, - MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, + static void EmitInitializeBody (PEAssemblyBuilder pe, + EntityHandle[] getExternalSpecs, EntityHandle[] getProxySpecs, 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); - } + var count = getExternalSpecs.Length; pe.EmitBody ("Initialize", MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, @@ -316,26 +298,10 @@ static MemberReferenceHandle AddTypeMappingMethodRef (PEAssemblyBuilder pe, Type 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, + static MemberReferenceHandle AddInitializeRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { var blob = new BlobBuilder (64); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 9c957562aca..4f42356e35d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -37,29 +37,27 @@ class TrimmableTypeMap } /// - /// 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 (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap) - { - 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). + /// Initializes the singleton with one or more typemap universes. + /// Called from the startup hook in the generated root assembly (_Microsoft.Android.TypeMaps). + /// When a single universe is provided (Release), uses directly. + /// When multiple universes are provided (Debug), wraps them in . /// 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})."); } + + if (typeMaps.Length == 1) { + InitializeCore (new SingleUniverseTypeMap (typeMaps [0], proxyMaps [0])); + return; + } + var universes = new SingleUniverseTypeMap [typeMaps.Length]; for (int i = 0; i < typeMaps.Length; i++) { universes [i] = new SingleUniverseTypeMap (typeMaps [i], proxyMaps [i]); From 126d65c03cc189734ba589752c95bac947827cb3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 22 Apr 2026 13:40:05 +0200 Subject: [PATCH 06/13] Revert "Consolidate Initialize to single array overload, update doc comments" This reverts commit a25b04a0c5ddbf828cdac480f21dc713c05c238e. --- .../Generator/RootTypeMapAssemblyGenerator.cs | 98 +++++++++++++------ .../TrimmableTypeMap.cs | 28 +++--- 2 files changed, 81 insertions(+), 45 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 4ea198865a0..fba57362e90 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -30,12 +30,12 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// { /// internal static void Initialize () /// { -/// // Option A: Shared universe (Release): +/// // Merged (single universe): /// TrimmableTypeMap.Initialize( -/// new[] { TypeMapping.GetOrCreateExternalTypeMapping<Java.Lang.Object>() }, -/// new[] { TypeMapping.GetOrCreateProxyTypeMapping<Java.Lang.Object>() }); +/// TypeMapping.GetOrCreateExternalTypeMapping<__TypeMapAnchor>(), +/// TypeMapping.GetOrCreateProxyTypeMapping<__TypeMapAnchor>()); /// -/// // Option B: Per-assembly universes, aggregated (Debug): +/// // Per-assembly (aggregate universes): /// var typeMaps = new IReadOnlyDictionary<string, Type>[] { /// TypeMapping.GetOrCreateExternalTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>(), /// TypeMapping.GetOrCreateExternalTypeMapping<_MyApp_TypeMap.__TypeMapAnchor>(), @@ -177,43 +177,61 @@ static void EmitStartupHook (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); - // TrimmableTypeMap.Initialize(IReadOnlyDictionary[], IReadOnlyDictionary[]) - // Both modes use the same overload — shared universe passes 1-element arrays. - var initializeRef = AddInitializeRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - - // Resolve anchor type(s) to method specs - EntityHandle[] getExternalSpecs; - EntityHandle[] getProxySpecs; if (useSharedTypemapUniverse) { - // Single shared universe — 1-element arrays using the shared anchor (Java.Lang.Object) - getExternalSpecs = new EntityHandle[] { MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle) }; - getProxySpecs = new EntityHandle[] { MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle) }; + // TrimmableTypeMap.Initialize(IReadOnlyDictionary, IReadOnlyDictionary) + var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); } else { - // Per-assembly universes — one element per assembly, each with its own __TypeMapAnchor - var count = perAssemblyTypeMapNames.Count; - getExternalSpecs = new EntityHandle [count]; - 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); - } + // 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); - EmitInitializeBody (pe, getExternalSpecs, getProxySpecs, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef); + pe.EmitBody ("Initialize", + MethodAttributes.Assembly | 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 EmitInitializeBody (PEAssemblyBuilder pe, - EntityHandle[] getExternalSpecs, EntityHandle[] getProxySpecs, + static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, + IReadOnlyList perAssemblyTypeMapNames, + MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, MemberReferenceHandle initializeRef, TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { - var count = getExternalSpecs.Length; + 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.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, @@ -298,10 +316,26 @@ static MemberReferenceHandle AddTypeMappingMethodRef (PEAssemblyBuilder pe, Type 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 AddInitializeRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, + static MemberReferenceHandle AddInitializeAggregateRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) { var blob = new BlobBuilder (64); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 4f42356e35d..9c957562aca 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -37,27 +37,29 @@ class TrimmableTypeMap } /// - /// Initializes the singleton with one or more typemap universes. - /// Called from the startup hook in the generated root assembly (_Microsoft.Android.TypeMaps). - /// When a single universe is provided (Release), uses directly. - /// When multiple universes are provided (Debug), wraps them in . + /// 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 (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap) + { + 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})."); } - - if (typeMaps.Length == 1) { - InitializeCore (new SingleUniverseTypeMap (typeMaps [0], proxyMaps [0])); - return; - } - var universes = new SingleUniverseTypeMap [typeMaps.Length]; for (int i = 0; i < typeMaps.Length; i++) { universes [i] = new SingleUniverseTypeMap (typeMaps [i], proxyMaps [i]); From 8af14884353323451baaa5d72b6319f31514dfd4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 22 Apr 2026 13:40:38 +0200 Subject: [PATCH 07/13] Update doc comments and add length-0 validation - Doc comments: Option A (shared universe) / Option B (per-assembly) - Show Java.Lang.Object as anchor for shared universe mode - Add length-zero check in array Initialize overload Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 8 ++++---- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index fba57362e90..41eb335c4ac 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -30,12 +30,12 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// { /// internal static void Initialize () /// { -/// // Merged (single universe): +/// // Option A: Shared universe /// TrimmableTypeMap.Initialize( -/// TypeMapping.GetOrCreateExternalTypeMapping<__TypeMapAnchor>(), -/// TypeMapping.GetOrCreateProxyTypeMapping<__TypeMapAnchor>()); +/// TypeMapping.GetOrCreateExternalTypeMapping<Java.Lang.Object>(), +/// TypeMapping.GetOrCreateProxyTypeMapping<Java.Lang.Object>()); /// -/// // Per-assembly (aggregate universes): +/// // Option B: Per-assembly universes (aggregated) /// var typeMaps = new IReadOnlyDictionary<string, Type>[] { /// TypeMapping.GetOrCreateExternalTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>(), /// TypeMapping.GetOrCreateExternalTypeMapping<_MyApp_TypeMap.__TypeMapAnchor>(), diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 9c957562aca..c89e8bbef74 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -57,6 +57,9 @@ internal static void Initialize (IReadOnlyDictionary[] typeMaps, I { 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})."); } From daa2235895816c701d09832ed6d02dc9fe91f02e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 22 Apr 2026 14:02:06 +0200 Subject: [PATCH 08/13] Address Copilot review: fix startup hook visibility, separator, unused using - Make Initialize() method public (startup hooks require public static) - Keep StartupHook class internal (allowed by runtime) - Use ':' literal instead of Path.PathSeparator for DOTNET_STARTUP_HOOKS composition (target is Android/Linux, not the build host) - Remove unused 'using System.Runtime.InteropServices' from ITypeMapWithAliasing.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/RootTypeMapAssemblyGenerator.cs | 4 ++-- .../Microsoft.Android.Runtime/ITypeMapWithAliasing.cs | 1 - .../targets/Microsoft.Android.Sdk.HotReload.targets | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 41eb335c4ac..e1763c9dd7f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -198,7 +198,7 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); pe.EmitBody ("Initialize", - MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { // TypeMapping.GetOrCreateExternalTypeMapping<__TypeMapAnchor>() @@ -234,7 +234,7 @@ static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, } pe.EmitBody ("Initialize", - MethodAttributes.Assembly | MethodAttributes.Static | MethodAttributes.HideBySig, + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { // var typeMaps = new IReadOnlyDictionary[N]; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs index fbe7538c2af..af516adb468 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Runtime.InteropServices; using Java.Interop; namespace Microsoft.Android.Runtime; 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 95f80596be8..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 @@ -44,13 +44,13 @@ See: https://github.com/dotnet/sdk/pull/52581 Update DOTNET_STARTUP_HOOKS in @(RuntimeEnvironmentVariable) to use just the assembly name. The full path doesn't work on Android since the DLL is deployed alongside the app. Preserve any existing DOTNET_STARTUP_HOOKS values (e.g., from trimmable typemap) by composing - with Path.PathSeparator. + with ':' (the path separator on Android/Linux). --> <_ExistingStartupHooks Include="@(RuntimeEnvironmentVariable)" Condition=" '%(Identity)' == 'DOTNET_STARTUP_HOOKS' and '%(Value)' != '$(_AndroidHotReloadAgentAssemblyPath)' " /> Date: Wed, 22 Apr 2026 14:05:19 +0200 Subject: [PATCH 09/13] Compose DOTNET_STARTUP_HOOKS with existing values in trimmable targets Don't clobber existing DOTNET_STARTUP_HOOKS entries (e.g., from VS tooling). Use the same compose pattern as HotReload targets with ':' separator. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Sdk.TypeMap.Trimmable.targets | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) 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 cd2c296fa61..cc9d5a6d3f9 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 @@ -29,8 +29,16 @@ Value="true" Trim="true" /> - - + + <_ExistingStartupHooks Include="@(RuntimeEnvironmentVariable)" Condition=" '%(Identity)' == 'DOTNET_STARTUP_HOOKS' " /> + + + + <_ExistingStartupHooks Remove="@(_ExistingStartupHooks)" /> From 1d0f6764fab2538dc59dc2cfc20b734560d1fbca Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 22 Apr 2026 14:33:35 +0200 Subject: [PATCH 10/13] Address review: remove DCL, split types into separate files - Remove outer null check in InitializeCore (single-threaded init via startup hook, lock alone is sufficient) - Split ITypeMapWithAliasing.cs into three files: ITypeMapWithAliasing.cs, SingleUniverseTypeMap.cs, AggregateTypeMap.cs Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AggregateTypeMap.cs | 44 +++++++ .../ITypeMapWithAliasing.cs | 116 ------------------ .../SingleUniverseTypeMap.cs | 86 +++++++++++++ .../TrimmableTypeMap.cs | 4 - 4 files changed, 130 insertions(+), 120 deletions(-) create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs 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 index af516adb468..0849741e96d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs @@ -3,8 +3,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using Java.Interop; namespace Microsoft.Android.Runtime; @@ -29,117 +27,3 @@ interface ITypeMapWithAliasing /// bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? proxyType); } - -/// -/// 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; - } -} - -/// -/// 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/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 c89e8bbef74..2272fc1d9dc 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -72,10 +72,6 @@ internal static void Initialize (IReadOnlyDictionary[] typeMaps, I static void InitializeCore (ITypeMapWithAliasing typeMap) { - if (s_instance is not null) { - throw new InvalidOperationException ("TrimmableTypeMap has already been initialized."); - } - lock (s_initLock) { if (s_instance is not null) { throw new InvalidOperationException ("TrimmableTypeMap has already been initialized."); From bb3885aac7985196ec4cb52bf248da8d908f1cd2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 22 Apr 2026 15:11:12 +0200 Subject: [PATCH 11/13] Add new files to Mono.Android.csproj explicit includes Mono.Android uses explicit Compile includes, not globbing. Add the three new files split from ITypeMapWithAliasing.cs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Mono.Android.csproj | 3 +++ 1 file changed, 3 insertions(+) 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 @@ + + + From dacfa79316c70f5768340f6970cef8a20e87eacc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 22 Apr 2026 18:11:19 +0200 Subject: [PATCH 12/13] Fix MSB4190: remove %(Identity) from static ItemGroup condition MSBuild does not allow built-in metadata references like %(Identity) in Condition attributes of static ItemGroup elements. This caused all trimmable typemap builds to fail during evaluation with: error MSB4190: The reference to the built-in metadata "Identity" at position 2 is not allowed in this condition. Simplify to just add the startup hook directly. Other targets (HotReload) compose on top inside proper Target elements where %(Identity) is allowed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Sdk.TypeMap.Trimmable.targets | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) 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 cc9d5a6d3f9..ad7137f2842 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 @@ -29,16 +29,9 @@ Value="true" Trim="true" /> - - <_ExistingStartupHooks Include="@(RuntimeEnvironmentVariable)" Condition=" '%(Identity)' == 'DOTNET_STARTUP_HOOKS' " /> - - - - <_ExistingStartupHooks Remove="@(_ExistingStartupHooks)" /> + + From 64bf9aef8593afc29cb73be4e466bbcf523c5f29 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 22 Apr 2026 18:38:57 +0200 Subject: [PATCH 13/13] Move startup hook composition to Target to fix MSB4190 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit simplified too aggressively — just adding a static RuntimeEnvironmentVariable item would overwrite any existing DOTNET_STARTUP_HOOKS entries (e.g. from VS tooling). Move the compose-with-existing logic into a proper Target where %(Identity) metadata batching is allowed. The Target runs before _AndroidConfigureHotReloadEnvironment so HotReload can compose on top. Verified locally: all 3 TrimmableTypeMapBuildTests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) 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 ad7137f2842..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 @@ -29,14 +29,33 @@ Value="true" Trim="true" /> - - + + + + <_ExistingStartupHooks Include="@(RuntimeEnvironmentVariable)" Condition=" '%(Identity)' == 'DOTNET_STARTUP_HOOKS' " /> + + + + <_ExistingStartupHooks Remove="@(_ExistingStartupHooks)" /> + + +