diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs
index e1763c9dd7f..d6b7bae9a22 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs
@@ -11,7 +11,7 @@ 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 calls
+///
- Emits a TypeMapLoader class whose Initialize() method calls
/// with the appropriate
/// type mapping dictionaries.
///
@@ -25,26 +25,29 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;
/// [assembly: TypeMapAssemblyTarget<__TypeMapAnchor>("_Mono.Android.TypeMap")]
/// [assembly: TypeMapAssemblyTarget<__TypeMapAnchor>("_MyApp.TypeMap")]
///
-/// // Startup hook — called by DOTNET_STARTUP_HOOKS:
-/// internal static class StartupHook
+/// namespace Microsoft.Android.Runtime
/// {
-/// internal static void Initialize ()
+/// // Called directly from JNIEnvInit.Initialize():
+/// public static class TypeMapLoader
/// {
-/// // Option A: Shared universe
-/// TrimmableTypeMap.Initialize(
-/// TypeMapping.GetOrCreateExternalTypeMapping<Java.Lang.Object>(),
-/// TypeMapping.GetOrCreateProxyTypeMapping<Java.Lang.Object>());
+/// public static void Initialize ()
+/// {
+/// // Option A: Shared universe
+/// TrimmableTypeMap.Initialize(
+/// TypeMapping.GetOrCreateExternalTypeMapping<Java.Lang.Object>(),
+/// TypeMapping.GetOrCreateProxyTypeMapping<Java.Lang.Object>());
///
-/// // Option B: Per-assembly universes (aggregated)
-/// var typeMaps = new IReadOnlyDictionary<string, Type>[] {
-/// TypeMapping.GetOrCreateExternalTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>(),
-/// TypeMapping.GetOrCreateExternalTypeMapping<_MyApp_TypeMap.__TypeMapAnchor>(),
-/// };
-/// var proxyMaps = new IReadOnlyDictionary<Type, Type>[] {
-/// TypeMapping.GetOrCreateProxyTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>(),
-/// TypeMapping.GetOrCreateProxyTypeMapping<_MyApp_TypeMap.__TypeMapAnchor>(),
-/// };
-/// TrimmableTypeMap.Initialize(typeMaps, proxyMaps);
+/// // Option B: Per-assembly universes (aggregated)
+/// var typeMaps = new IReadOnlyDictionary<string, Type>[] {
+/// TypeMapping.GetOrCreateExternalTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>(),
+/// TypeMapping.GetOrCreateExternalTypeMapping<_MyApp_TypeMap.__TypeMapAnchor>(),
+/// };
+/// var proxyMaps = new IReadOnlyDictionary<Type, Type>[] {
+/// TypeMapping.GetOrCreateProxyTypeMapping<_Mono_Android_TypeMap.__TypeMapAnchor>(),
+/// TypeMapping.GetOrCreateProxyTypeMapping<_MyApp_TypeMap.__TypeMapAnchor>(),
+/// };
+/// TrimmableTypeMap.Initialize(typeMaps, proxyMaps);
+/// }
/// }
/// }
///
@@ -108,8 +111,8 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha
// 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,
+ // Emit [assembly: IgnoresAccessChecksTo("...")] so TypeMapLoader.Initialize() can access
+ // internal types (SingleUniverseTypeMap, AggregateTypeMap in Mono.Android,
// and __TypeMapAnchor in each per-assembly typemap DLL).
var accessTargets = new List { "Mono.Android" };
if (!useSharedTypemapUniverse) {
@@ -117,8 +120,8 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha
}
pe.EmitIgnoresAccessChecksToAttribute (accessTargets);
- // Emit StartupHook class with Initialize() method
- EmitStartupHook (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse);
+ // Emit TypeMapLoader class with Initialize() method
+ EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse);
pe.WritePE (stream);
}
@@ -142,7 +145,7 @@ static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anc
}
}
- static void EmitStartupHook (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse)
+ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse)
{
var metadata = pe.Metadata;
@@ -167,11 +170,11 @@ static void EmitStartupHook (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle
var getProxyMemberRef = AddTypeMappingMethodRef (pe, typeMappingRef, "GetOrCreateProxyTypeMapping",
iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false);
- // Define the StartupHook type
+ // Define the TypeMapLoader type (public static class in Microsoft.Android.Runtime namespace)
metadata.AddTypeDefinition (
- TypeAttributes.NotPublic | TypeAttributes.Sealed | TypeAttributes.Abstract | TypeAttributes.Class,
- default,
- metadata.GetOrAddString ("StartupHook"),
+ TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Abstract | TypeAttributes.Class,
+ metadata.GetOrAddString ("Microsoft.Android.Runtime"),
+ metadata.GetOrAddString ("TypeMapLoader"),
metadata.AddTypeReference (pe.SystemRuntimeRef,
metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Object")),
MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1),
diff --git a/src/Microsoft.Android.TypeMaps.Ref/Microsoft.Android.TypeMaps.Ref.csproj b/src/Microsoft.Android.TypeMaps.Ref/Microsoft.Android.TypeMaps.Ref.csproj
new file mode 100644
index 00000000000..a9830cdb95a
--- /dev/null
+++ b/src/Microsoft.Android.TypeMaps.Ref/Microsoft.Android.TypeMaps.Ref.csproj
@@ -0,0 +1,16 @@
+
+
+
+
+ $(DotNetTargetFramework)
+ _Microsoft.Android.TypeMaps
+ Microsoft.Android.Runtime
+ enable
+
+ true
+
+
+
+
diff --git a/src/Microsoft.Android.TypeMaps.Ref/TypeMapLoader.cs b/src/Microsoft.Android.TypeMaps.Ref/TypeMapLoader.cs
new file mode 100644
index 00000000000..8060cdbebe8
--- /dev/null
+++ b/src/Microsoft.Android.TypeMaps.Ref/TypeMapLoader.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Microsoft.Android.Runtime;
+
+///
+/// Entry point for typemap initialization. The implementation is generated at build time
+/// by the trimmable typemap generator, replacing this reference assembly.
+///
+public static class TypeMapLoader
+{
+ ///
+ /// Initializes the trimmable typemap by constructing the type mapping dictionaries
+ /// and calling TrimmableTypeMap.Initialize().
+ ///
+ public static void Initialize () => throw new NotImplementedException (
+ "This is a reference assembly stub. The real implementation is generated at build time.");
+}
diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs
index 52677116363..f59a8568f66 100644
--- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs
+++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs
@@ -179,6 +179,9 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
if (!RuntimeFeature.TrimmableTypeMap) {
args->registerJniNativesFn = (IntPtr)(delegate* unmanaged)&RegisterJniNatives;
}
+ if (RuntimeFeature.TrimmableTypeMap) {
+ InitializeTrimmableTypeMap ();
+ }
RunStartupHooksIfNeeded ();
SetSynchronizationContext ();
}
@@ -187,6 +190,14 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
[UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })]
private static unsafe partial void xamarin_app_init (IntPtr env, delegate* unmanaged get_function_pointer);
+ // Separate method so the JIT doesn't try to resolve TypeMapLoader (from _Microsoft.Android.TypeMaps.dll)
+ // when compiling JNIEnvInit.Initialize() in non-trimmable builds where that assembly isn't present.
+ [MethodImpl (MethodImplOptions.NoInlining)]
+ static void InitializeTrimmableTypeMap ()
+ {
+ TypeMapLoader.Initialize ();
+ }
+
static void RunStartupHooksIfNeeded ()
{
// Return if startup hooks are disabled or not CoreCLR
diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
index 2272fc1d9dc..7b94c8f2184 100644
--- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
+++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
@@ -17,7 +17,7 @@ namespace Microsoft.Android.Runtime;
/// and provides peer creation, invoker resolution, container factories, and native
/// method registration. All proxy attribute access is encapsulated here.
///
-class TrimmableTypeMap
+public class TrimmableTypeMap
{
static readonly Lock s_initLock = new ();
static readonly JavaPeerProxy s_noPeerSentinel = new MissingJavaPeerProxy ();
@@ -38,10 +38,10 @@ 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).
+ /// Called from in the generated root assembly
+ /// (_Microsoft.Android.TypeMaps) when assembly typemaps are merged (Release builds).
///
- internal static void Initialize (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap)
+ public static void Initialize (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap)
{
ArgumentNullException.ThrowIfNull (typeMap);
ArgumentNullException.ThrowIfNull (proxyMap);
@@ -50,10 +50,10 @@ internal static void Initialize (IReadOnlyDictionary typeMap, IRea
///
/// 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).
+ /// Called from 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)
+ public static void Initialize (IReadOnlyDictionary[] typeMaps, IReadOnlyDictionary[] proxyMaps)
{
ArgumentNullException.ThrowIfNull (typeMaps);
ArgumentNullException.ThrowIfNull (proxyMaps);
diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj
index 55352540d70..89bf52ac3b1 100644
--- a/src/Mono.Android/Mono.Android.csproj
+++ b/src/Mono.Android/Mono.Android.csproj
@@ -61,6 +61,12 @@
+
+
+ false
+ all
+
ILLink.LinkAttributes.xml
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 4156e0db1b5..7654d9bf248 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,7 +43,7 @@ See: https://github.com/dotnet/sdk/pull/52581
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 40a2c1519c3..52ce49be5cf 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,9 +11,6 @@
<_TypeMapAssemblyName>_Microsoft.Android.TypeMaps
-
- true
@@ -34,28 +31,6 @@
Condition=" '$(_AndroidRuntime)' == 'CoreCLR' " />
-
-
-
- <_ExistingStartupHooks Include="@(RuntimeEnvironmentVariable)" Condition=" '%(Identity)' == 'DOTNET_STARTUP_HOOKS' " />
-
-
-
- <_ExistingStartupHooks Remove="@(_ExistingStartupHooks)" />
-
-
-
diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs
index 0acd64e1812..84dd6a0a2f4 100644
--- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs
+++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs
@@ -139,11 +139,13 @@ public void Generate_BothMergeModes_ProduceValidPEAssembly (bool useSharedTypema
var reader = pe.GetMetadataReader ();
- // Both modes should have StartupHook type with Initialize method
+ // Both modes should have TypeMapLoader type in the correct namespace, with public visibility and Initialize method
var typeDefs = reader.TypeDefinitions
.Select (h => reader.GetTypeDefinition (h))
.ToList ();
- Assert.Contains (typeDefs, t => reader.GetString (t.Name) == "StartupHook");
+ var typeMapLoader = typeDefs.Single (t => reader.GetString (t.Name) == "TypeMapLoader");
+ Assert.Equal ("Microsoft.Android.Runtime", reader.GetString (typeMapLoader.Namespace));
+ Assert.True (typeMapLoader.Attributes.HasFlag (System.Reflection.TypeAttributes.Public));
// Both modes should have assembly target attributes
var targetAttrs = GetTypeMapAssemblyTargetAttributes (reader);