Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;
/// Generates the root <c>_Microsoft.Android.TypeMaps.dll</c> assembly that:
/// <list type="bullet">
/// <item>References all per-assembly typemap assemblies via <c>[assembly: TypeMapAssemblyTargetAttribute&lt;__TypeMapAnchor&gt;("name")]</c>.</item>
/// <item>Emits a <c>StartupHook</c> class whose <c>Initialize()</c> method calls
/// <item>Emits a <c>TypeMapLoader</c> class whose <c>Initialize()</c> method calls
/// <see cref="Microsoft.Android.Runtime.TrimmableTypeMap.Initialize"/> with the appropriate
/// type mapping dictionaries.</item>
/// </list>
Expand All @@ -25,26 +25,29 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap;
/// [assembly: TypeMapAssemblyTarget&lt;__TypeMapAnchor&gt;("_Mono.Android.TypeMap")]
/// [assembly: TypeMapAssemblyTarget&lt;__TypeMapAnchor&gt;("_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&lt;Java.Lang.Object&gt;(),
/// TypeMapping.GetOrCreateProxyTypeMapping&lt;Java.Lang.Object&gt;());
/// public static void Initialize ()
/// {
/// // Option A: Shared universe
/// TrimmableTypeMap.Initialize(
/// TypeMapping.GetOrCreateExternalTypeMapping&lt;Java.Lang.Object&gt;(),
/// TypeMapping.GetOrCreateProxyTypeMapping&lt;Java.Lang.Object&gt;());
///
/// // Option B: Per-assembly universes (aggregated)
/// var typeMaps = new IReadOnlyDictionary&lt;string, Type&gt;[] {
/// TypeMapping.GetOrCreateExternalTypeMapping&lt;_Mono_Android_TypeMap.__TypeMapAnchor&gt;(),
/// TypeMapping.GetOrCreateExternalTypeMapping&lt;_MyApp_TypeMap.__TypeMapAnchor&gt;(),
/// };
/// var proxyMaps = new IReadOnlyDictionary&lt;Type, Type&gt;[] {
/// TypeMapping.GetOrCreateProxyTypeMapping&lt;_Mono_Android_TypeMap.__TypeMapAnchor&gt;(),
/// TypeMapping.GetOrCreateProxyTypeMapping&lt;_MyApp_TypeMap.__TypeMapAnchor&gt;(),
/// };
/// TrimmableTypeMap.Initialize(typeMaps, proxyMaps);
/// // Option B: Per-assembly universes (aggregated)
/// var typeMaps = new IReadOnlyDictionary&lt;string, Type&gt;[] {
/// TypeMapping.GetOrCreateExternalTypeMapping&lt;_Mono_Android_TypeMap.__TypeMapAnchor&gt;(),
/// TypeMapping.GetOrCreateExternalTypeMapping&lt;_MyApp_TypeMap.__TypeMapAnchor&gt;(),
/// };
/// var proxyMaps = new IReadOnlyDictionary&lt;Type, Type&gt;[] {
/// TypeMapping.GetOrCreateProxyTypeMapping&lt;_Mono_Android_TypeMap.__TypeMapAnchor&gt;(),
/// TypeMapping.GetOrCreateProxyTypeMapping&lt;_MyApp_TypeMap.__TypeMapAnchor&gt;(),
/// };
/// TrimmableTypeMap.Initialize(typeMaps, proxyMaps);
/// }
/// }
/// }
/// </code>
Expand Down Expand Up @@ -108,17 +111,17 @@ public void Generate (IReadOnlyList<string> 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<string> { "Mono.Android" };
if (!useSharedTypemapUniverse) {
accessTargets.AddRange (perAssemblyTypeMapNames);
}
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);
}
Expand All @@ -142,7 +145,7 @@ static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anc
}
}

static void EmitStartupHook (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList<string> perAssemblyTypeMapNames, bool useSharedTypemapUniverse)
static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList<string> perAssemblyTypeMapNames, bool useSharedTypemapUniverse)
{
var metadata = pe.Metadata;

Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\Configuration.props" />

<PropertyGroup>
<TargetFramework>$(DotNetTargetFramework)</TargetFramework>
<AssemblyName>_Microsoft.Android.TypeMaps</AssemblyName>
<RootNamespace>Microsoft.Android.Runtime</RootNamespace>
<Nullable>enable</Nullable>
<!-- ProduceOnlyReferenceAssembly (not ProduceReferenceAssembly) — we only need the ref assembly.
The real implementation is generated at app build time by the typemap generator. -->
<ProduceOnlyReferenceAssembly>true</ProduceOnlyReferenceAssembly>
Comment thread
simonrozsival marked this conversation as resolved.
<!-- Do NOT sign — the generated implementation assembly (emitted by PEAssemblyBuilder)
is unsigned, so the ref assembly must match to avoid identity mismatch. -->
</PropertyGroup>

</Project>
17 changes: 17 additions & 0 deletions src/Microsoft.Android.TypeMaps.Ref/TypeMapLoader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

namespace Microsoft.Android.Runtime;

/// <summary>
/// Entry point for typemap initialization. The implementation is generated at build time
/// by the trimmable typemap generator, replacing this reference assembly.
/// </summary>
public static class TypeMapLoader
{
/// <summary>
/// Initializes the trimmable typemap by constructing the type mapping dictionaries
/// and calling <c>TrimmableTypeMap.Initialize()</c>.
/// </summary>
public static void Initialize () => throw new NotImplementedException (
"This is a reference assembly stub. The real implementation is generated at build time.");
}
11 changes: 11 additions & 0 deletions src/Mono.Android/Android.Runtime/JNIEnvInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
if (!RuntimeFeature.TrimmableTypeMap) {
args->registerJniNativesFn = (IntPtr)(delegate* unmanaged<IntPtr, int, IntPtr, IntPtr, int, void>)&RegisterJniNatives;
}
if (RuntimeFeature.TrimmableTypeMap) {
InitializeTrimmableTypeMap ();
}
RunStartupHooksIfNeeded ();
SetSynchronizationContext ();
}
Expand All @@ -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 <int, int, int, IntPtr*, void> 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
Expand Down
14 changes: 7 additions & 7 deletions src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
class TrimmableTypeMap
public class TrimmableTypeMap
{
static readonly Lock s_initLock = new ();
static readonly JavaPeerProxy s_noPeerSentinel = new MissingJavaPeerProxy ();
Expand All @@ -38,10 +38,10 @@ class TrimmableTypeMap

/// <summary>
/// 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 <see cref="TypeMapLoader.Initialize"/> in the generated root assembly
/// (_Microsoft.Android.TypeMaps) when assembly typemaps are merged (Release builds).
/// </summary>
internal static void Initialize (IReadOnlyDictionary<string, Type> typeMap, IReadOnlyDictionary<Type, Type> proxyMap)
public static void Initialize (IReadOnlyDictionary<string, Type> typeMap, IReadOnlyDictionary<Type, Type> proxyMap)
{
ArgumentNullException.ThrowIfNull (typeMap);
ArgumentNullException.ThrowIfNull (proxyMap);
Expand All @@ -50,10 +50,10 @@ internal static void Initialize (IReadOnlyDictionary<string, Type> typeMap, IRea

/// <summary>
/// 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 <see cref="TypeMapLoader.Initialize"/> in the generated root assembly
/// (_Microsoft.Android.TypeMaps) when each assembly has its own typemap universe (Debug builds).
/// </summary>
internal static void Initialize (IReadOnlyDictionary<string, Type>[] typeMaps, IReadOnlyDictionary<Type, Type>[] proxyMaps)
public static void Initialize (IReadOnlyDictionary<string, Type>[] typeMaps, IReadOnlyDictionary<Type, Type>[] proxyMaps)
{
ArgumentNullException.ThrowIfNull (typeMaps);
ArgumentNullException.ThrowIfNull (proxyMaps);
Expand Down
6 changes: 6 additions & 0 deletions src/Mono.Android/Mono.Android.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@

<ItemGroup>
<ProjectReference Include="..\..\external\Java.Interop\src\Java.Interop\Java.Interop.csproj" />
<!-- Compile-only reference: the real _Microsoft.Android.TypeMaps.dll is generated at app
build time by the trimmable typemap generator and replaces this ref assembly. -->
<ProjectReference Include="..\Microsoft.Android.TypeMaps.Ref\Microsoft.Android.TypeMaps.Ref.csproj">
<Private>false</Private>
<PrivateAssets>all</PrivateAssets>
</ProjectReference>
Comment thread
simonrozsival marked this conversation as resolved.
<EmbeddedResource Include="ILLink/ILLink.LinkAttributes.xml">
<LogicalName>ILLink.LinkAttributes.xml</LogicalName>
</EmbeddedResource>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ 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
Preserve any existing DOTNET_STARTUP_HOOKS values by composing
with ':' (the path separator on Android/Linux).
-->
<ItemGroup Condition=" '$(_AndroidHotReloadAgentAssemblyName)' != '' ">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@

<PropertyGroup>
<_TypeMapAssemblyName>_Microsoft.Android.TypeMaps</_TypeMapAssemblyName>
<!-- Enable startup hooks for trimmable typemap — the root assembly acts as a startup hook
that constructs the type mapping dictionaries and calls TrimmableTypeMap.Initialize(). -->
<StartupHookSupport>true</StartupHookSupport>
<!-- Use the outer IntermediateOutputPath when available (inner per-RID builds set
_OuterIntermediateOutputPath) so that generated manifests/JCWs are written
where the packaging phase expects them. -->
Expand All @@ -34,28 +31,6 @@
Condition=" '$(_AndroidRuntime)' == 'CoreCLR' " />
</ItemGroup>

<!--
Configure the root typemap assembly as a startup hook.
This must run inside a Target (not a static ItemGroup) because composing with
existing DOTNET_STARTUP_HOOKS entries requires %(Identity) metadata batching,
which is only allowed inside Target ItemGroups (MSB4190).
Runs before _AndroidConfigureHotReloadEnvironment so HotReload can compose on top.
-->
<Target Name="_ConfigureTrimmableTypeMapStartupHook"
BeforeTargets="_AndroidConfigureHotReloadEnvironment;_GenerateEnvironmentFiles">
<ItemGroup>
<_ExistingStartupHooks Include="@(RuntimeEnvironmentVariable)" Condition=" '%(Identity)' == 'DOTNET_STARTUP_HOOKS' " />
<RuntimeEnvironmentVariable Remove="DOTNET_STARTUP_HOOKS" />
<RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS"
Value="$(_TypeMapAssemblyName):@(_ExistingStartupHooks->'%(Value)', ':')"
Condition=" '@(_ExistingStartupHooks)' != '' " />
<RuntimeEnvironmentVariable Include="DOTNET_STARTUP_HOOKS"
Value="$(_TypeMapAssemblyName)"
Condition=" '@(_ExistingStartupHooks)' == '' " />
<_ExistingStartupHooks Remove="@(_ExistingStartupHooks)" />
</ItemGroup>
</Target>

<Target Name="_ValidateTrimmableTypeMapRuntime"
Condition=" '$(_AndroidRuntime)' != 'CoreCLR' And '$(_AndroidRuntime)' != 'NativeAOT' "
BeforeTargets="Build">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down