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
279 changes: 274 additions & 5 deletions Xamarin.Android-Tests.sln

Large diffs are not rendered by default.

39 changes: 38 additions & 1 deletion src/Mono.Android/Android.Runtime/JNIEnvInit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,50 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args)
}

args->propagateUncaughtExceptionFn = (IntPtr)(delegate* unmanaged<IntPtr, IntPtr, IntPtr, void>)&PropagateUncaughtException;

RunStartupHooksIfNeeded ();
SetSynchronizationContext ();
}

[DllImport (RuntimeConstants.InternalDllName, CallingConvention = CallingConvention.Cdecl)]
static extern unsafe void xamarin_app_init (IntPtr env, delegate* unmanaged <int, int, int, IntPtr*, void> get_function_pointer);

static void RunStartupHooksIfNeeded ()
{
// Return if startup hooks are disabled or not CoreCLR
if (!RuntimeFeature.IsCoreClrRuntime)
return;
if (!RuntimeFeature.StartupHookSupport)
return;

RunStartupHooks ();
}

[RequiresUnreferencedCode ("Uses reflection to access System.StartupHookProvider.")]
static void RunStartupHooks ()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a test to ensure this method is really trimmed away for a Release build (or when startuphook support is not enabled)?

Copy link
Member Author

@jonathanpeppers jonathanpeppers Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we run the new test in both trimmed, AOT, all runtimes, etc.

They have a line in dotnet/runtime to preserve it, too:

https://github.com/dotnet/runtime/blob/eaf635955da0410b94cb8607f6e598c51245f830/src/mono/System.Private.CoreLib/src/ILLink/ILLink.Descriptors.xml#L656-L663

{
const string typeName = "System.StartupHookProvider";
const string methodName = "ProcessStartupHooks";

var type = typeof(object).Assembly.GetType (typeName, throwOnError: false);
if (type is null) {
RuntimeNativeMethods.monodroid_log (LogLevel.Warn, LogCategories.Default,
$"Could not load type '{typeName}'. Skipping startup hooks.");
return;
}

var method = type.GetMethod (methodName,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to use the [UnsafeAccessor] attribute to avoid using reflection here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you can only do that if a type is public, and it's internal in System.Private.CoreLib.

BindingFlags.NonPublic | BindingFlags.Static, null, [ typeof(string) ], null);
if (method is null) {
RuntimeNativeMethods.monodroid_log (LogLevel.Warn, LogCategories.Default,
$"Could not load method '{typeName}.{methodName}'. Skipping startup hooks.");
return;
}

// Pass empty string for diagnosticStartupHooks parameter
// The method will read STARTUP_HOOKS from AppContext internally
method.Invoke (null, [ "" ]);
}

static void SetSynchronizationContext () =>
SynchronizationContext.SetSynchronizationContext (Android.App.Application.SynchronizationContext);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ static class RuntimeFeature
const bool IsMonoRuntimeEnabledByDefault = true;
const bool IsCoreClrRuntimeEnabledByDefault = false;
const bool IsAssignableFromCheckEnabledByDefault = true;
const bool StartupHookSupportEnabledByDefault = true;

const string FeatureSwitchPrefix = "Microsoft.Android.Runtime.RuntimeFeature.";
const string StartupHookProviderSwitch = "System.StartupHookProvider.IsSupported";

[FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (ManagedTypeMap)}")]
internal static bool ManagedTypeMap { get; } =
Expand All @@ -27,4 +29,9 @@ static class RuntimeFeature
[FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (IsAssignableFromCheck)}")]
internal static bool IsAssignableFromCheck { get; } =
AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (IsAssignableFromCheck)}", out bool isEnabled) ? isEnabled : IsAssignableFromCheckEnabledByDefault;

[FeatureSwitchDefinition (StartupHookProviderSwitch)]
[FeatureGuard (typeof (RequiresUnreferencedCodeAttribute))]
internal static bool StartupHookSupport { get; } =
AppContext.TryGetSwitch (StartupHookProviderSwitch, out bool isEnabled) ? isEnabled : StartupHookSupportEnabledByDefault;
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@
<UseSystemResourceKeys Condition="'$(UseSystemResourceKeys)' == '' and '$(PublishTrimmed)' == 'true'">true</UseSystemResourceKeys>
<Http3Support Condition="'$(Http3Support)' == ''">false</Http3Support>
<InvariantGlobalization Condition="'$(InvariantGlobalization)' == ''">false</InvariantGlobalization>
<StartupHookSupport Condition="'$(StartupHookSupport)' == ''">false</StartupHookSupport>
<UseSizeOptimizedLinq Condition="'$(UseSizeOptimizedLinq)' == ''">true</UseSizeOptimizedLinq>
<UseNativeHttpHandler Condition=" $(AndroidHttpClientHandlerType.Contains ('System.Net.Http.SocketsHttpHandler')) And '$(UseNativeHttpHandler)' == '' ">false</UseNativeHttpHandler>
<UseNativeHttpHandler Condition="'$(UseNativeHttpHandler)' == ''">true</UseNativeHttpHandler>
Expand All @@ -156,6 +155,8 @@
<!-- Trimmer switches that default to OFF in Release mode -->
<PropertyGroup Condition=" '$(AndroidApplication)' == 'true' and '$(Optimize)' == 'true' ">
<DebuggerSupport Condition=" '$(DebuggerSupport)' == '' ">false</DebuggerSupport>
<!-- Used for Hot Reload -->
<StartupHookSupport Condition=" '$(StartupHookSupport)' == '' ">false</StartupHookSupport>
<HttpActivityPropagationSupport Condition=" '$(HttpActivityPropagationSupport)' == '' ">false</HttpActivityPropagationSupport>
<!-- Only set if *not* using $(EnableDiagnostics) -->
<MetricsSupport Condition=" '$(MetricsSupport)' == '' and '$(AndroidEnableProfiler)' != 'true' ">false</MetricsSupport>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<!-- Trimmer switches required for tests -->
<JsonSerializerIsReflectionEnabledByDefault Condition="'$(TrimMode)' == 'full'">true</JsonSerializerIsReflectionEnabledByDefault>
<_DefaultValueAttributeSupport Condition="'$(TrimMode)' == 'full'">true</_DefaultValueAttributeSupport>
<StartupHookSupport>true</StartupHookSupport>
</PropertyGroup>

<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
Expand All @@ -61,13 +62,15 @@
<ItemGroup>
<ProjectReference Include="..\..\TestRunner.Core\TestRunner.Core.NET.csproj" />
<ProjectReference Include="..\..\TestRunner.NUnit\TestRunner.NUnit.NET.csproj" />
<ProjectReference Include="..\..\StartupHook\StartupHook.csproj" />
<ProjectReference Include="..\Java.Interop-Tests\Java.Interop-Tests.NET.csproj" />
<ProjectReference Include="..\Mono.Android-Test.Library\Mono.Android-Test.Library.NET.csproj" />
<ProjectReference Include="..\..\..\src\Xamarin.Android.NUnitLite\Xamarin.Android.NUnitLite.NET.csproj" />
</ItemGroup>

<ItemGroup>
<TrimmerRootAssembly Include="Java.Interop-Tests" RootMode="All" />
<TrimmerRootAssembly Include="StartupHook" RootMode="All" />
<_AndroidRemapMembers Include="Remaps.xml" />
<_AndroidRemapMembers Include="IsAssignableFromRemaps.xml" Condition=" '$(_AndroidIsAssignableFromCheck)' == 'false' " />
<ProguardConfiguration Include="proguard.cfg" />
Expand Down Expand Up @@ -103,6 +106,7 @@
<Compile Include="System\AppDomainTest.cs" />
<Compile Include="System\AssemblyInformationalVersionAttributeTest.cs" />
<Compile Include="System\ExceptionTest.cs" />
<Compile Include="System\StartupHookTest.cs" />
<Compile Include="System\TimeZoneTest.cs" />
<Compile Include="System.Drawing\TypeConverterTest.cs" />
<Compile Include="System.IO\DirectoryTest.cs" />
Expand Down Expand Up @@ -227,6 +231,7 @@
<RuntimeHostConfigurationOption Include="test_bool" Value="true" />
<RuntimeHostConfigurationOption Include="test_integer" Value="42" />
<RuntimeHostConfigurationOption Include="test_string" Value="foo" />
<RuntimeHostConfigurationOption Include="STARTUP_HOOKS" Value="StartupHook" />
</ItemGroup>

<ItemGroup Condition=" '$(AndroidPackageFormat)' != 'aab' ">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using System;
using System.Reflection;
using NUnit.Framework;

namespace SystemTests
{
[TestFixture]
public class StartupHookTest
{
[Test]
public void IsInitialized ()
{
var type = Type.GetType ("StartupHook, StartupHook", throwOnError: true);
Assert.IsNotNull (type, "StartupHook type should be loaded");

var property = type.GetProperty ("IsInitialized", BindingFlags.Public | BindingFlags.Static);
Assert.IsNotNull (property, "IsInitialized property should exist");

var value = (bool) property.GetValue (null);
Assert.IsTrue (value, "StartupHook.Initialize() should have been called");
}
}
}
1 change: 1 addition & 0 deletions tests/Mono.Android-Tests/Mono.Android-Tests/env.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Environment Variables and system properties
# debug.mono.log=gref,default
debug.mono.debug=1
DOTNET_STARTUP_HOOKS=StartupHook
13 changes: 13 additions & 0 deletions tests/StartupHook/StartupHook.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;

internal static class StartupHook
{
public static bool IsInitialized { get; private set; }

public static void Initialize ()
{
Console.WriteLine ("StartupHook.Initialize() called");

IsInitialized = true;
}
}
10 changes: 10 additions & 0 deletions tests/StartupHook/StartupHook.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk" >

<PropertyGroup>
<TargetFramework>$(DotNetAndroidTargetFramework)</TargetFramework>
<SupportedOSPlatformVersion>$(AndroidMinimumDotNetApiLevel)</SupportedOSPlatformVersion>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<AndroidGenerateResourceDesigner>false</AndroidGenerateResourceDesigner>
</PropertyGroup>

</Project>
Loading