Skip to content

Commit a26e2d4

Browse files
rolfbjarneCopilotCopilot
authored
[linker] Convert the EnsureUIThread optimization to a trimmer feature switch. Fixes #26104. (#26140)
Replace the hand-written IL rewrite of `[NS|UI]Application.EnsureUIThread` with an `ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls` trimmer feature switch, driven by a new `$(CheckForIllegalCrossThreadCalls)` MSBuild property (defaults to the build configuration: on for debug, off for release). * Remove `ProcessEnsureUIThread` (and its case) from the linker's `OptimizeGeneratedCode` step, and mark the now-orphaned `--optimize=remove-uithread-checks` flag as removed (following the existing `inline-intptr-size` convention). * Emit the `ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls` `RuntimeHostConfigurationOption` from `Xamarin.Shared.Sdk.targets` and stub the `EnsureUIThread` method body via ILLink substitutions on all four platforms when the feature is off. * Make `[NS|UI]Application.CheckForIllegalCrossThreadCalls` trimmable when the checks are off, without an API break: drop the field's `= true` initializer and instead set it from the application startup path, guarded by an internal feature-switched `Runtime.CheckForIllegalCrossThreadCalls` getter. When the feature is off the getter substitutes to a constant `false`, so the assignment is dead-code-eliminated and — with `EnsureUIThread` also stubbed — the field can be trimmed away entirely. * Document the new `$(CheckForIllegalCrossThreadCalls)` property and update `optimizations.md`. Because the property is emitted as a runtime feature switch, it also takes effect when trimming is disabled (the field reflects the property value at runtime); trimming is only needed to physically remove the check code. Add `EnsureUIThreadChecksTest` (with a dedicated `EnsureUIThreadApp` project that keeps `EnsureUIThread` reachable) verifying the feature switch value and that, in the app bundle, `EnsureUIThread` is stubbed and the field is trimmed when the checks are off (iOS + macOS). Fixes #26104 🤖 Pull request created by Copilot --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Rolf Bjarne Kvinge <rokvin@microsoft.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 3d00dc0 commit a26e2d4

43 files changed

Lines changed: 375 additions & 235 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/building-apps/build-properties.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,27 @@ The default value of this property `false` in .NET 9, and `true` in .NET 10+.
197197
> [!NOTE]
198198
> File an issue if you find that you need to disable this feature, as it's possible that the option to disable it will be removed in future.
199199
200+
## CheckForIllegalCrossThreadCalls
201+
202+
Controls whether the UI thread checks (the `[NS|UI]Application.EnsureUIThread`
203+
calls the generated bindings emit for UI code) are performed.
204+
205+
When set to `true`, the checks are enabled: accessing UI API off the UI thread
206+
throws an exception. When set to `false`, the checks are disabled.
207+
208+
This property is emitted as the `ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls`
209+
runtime feature switch, so it takes effect even when trimming is disabled: the
210+
`[NS|UI]Application.CheckForIllegalCrossThreadCalls` field reflects the property
211+
value at runtime, and the checks are enabled or disabled accordingly.
212+
213+
When trimming is enabled and the checks are disabled, ILLink additionally stubs
214+
the `[NS|UI]Application.EnsureUIThread` method body and trims away the
215+
`CheckForIllegalCrossThreadCalls` field, making the app slightly smaller and
216+
faster.
217+
218+
If this value is not specified, the checks are kept in debug builds and removed
219+
in release builds.
220+
200221
## CodesignAllocate
201222

202223
The path to the `codesign_allocate` tool.

docs/website/optimizations.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,15 @@ Xamarin.iOS and Xamarin.Mac apps.
1616

1717
## Remove UIApplication.EnsureUIThread / NSApplication.EnsureUIThread
1818

19-
Removes calls to [UIApplication.EnsureUIThread][1] (for Xamarin.iOS) or
20-
`NSApplication.EnsureUIThread` (for Xamarin.Mac).
19+
> [!NOTE]
20+
> This optimization has been replaced by the `CheckForIllegalCrossThreadCalls`
21+
> MSBuild property, which sets the
22+
> `ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls` trimmer feature switch.
23+
> The `--optimize=[+|-]remove-uithread-checks` flag is no longer applied and has
24+
> no effect.
25+
26+
Removes calls to [UIApplication.EnsureUIThread][1] (for .NET for iOS) or
27+
`NSApplication.EnsureUIThread` (for .NET for macOS).
2128

2229
This optimization will change the following type of code:
2330

@@ -43,7 +50,7 @@ methods with the `[BindingImpl (BindingImplOptions.Optimizable)]` attribute.
4350

4451
By default it's enabled for release builds.
4552

46-
The default behavior can be overridden by passing `--optimize=[+|-]remove-uithread-checks` to mtouch/mmp.
53+
Set the `CheckForIllegalCrossThreadCalls` MSBuild property to override the default behavior.
4754

4855
[1]: /dotnet/api/UIKit.UIApplication.EnsureUIThread
4956

dotnet/targets/Xamarin.Shared.Sdk.targets

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,15 @@
755755
<!-- Set default ValidateObjectPointers value -->
756756
<_ValidateObjectPointers Condition="'$(_ValidateObjectPointers)' == ''">false</_ValidateObjectPointers>
757757

758+
<!--
759+
Set the 'ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls' feature switch value. When false, ILLink stubs
760+
the [NS|UI]Application.EnsureUIThread method body (so the UI thread checks are removed). A user-specified
761+
$(CheckForIllegalCrossThreadCalls) always wins; otherwise we keep the checks in debug builds and remove them in
762+
release builds (matching the old 'remove-uithread-checks' optimization default).
763+
-->
764+
<_CheckForIllegalCrossThreadCalls Condition="'$(CheckForIllegalCrossThreadCalls)' != ''">$(CheckForIllegalCrossThreadCalls)</_CheckForIllegalCrossThreadCalls>
765+
<_CheckForIllegalCrossThreadCalls Condition="'$(_CheckForIllegalCrossThreadCalls)' == ''">$(_BundlerDebug)</_CheckForIllegalCrossThreadCalls>
766+
758767
<_CustomLinkerOptions>
759768
AreAnyAssembliesTrimmed=$(_AreAnyAssembliesTrimmed)
760769
AssemblyName=$(AssemblyName).dll
@@ -904,6 +913,7 @@
904913
<RuntimeHostConfigurationOption Include="ObjCRuntime.Runtime.IsCoreCLR" Value="true" Trim="true" Condition="'$(UseMonoRuntime)' != 'true'" />
905914
<RuntimeHostConfigurationOption Include="ObjCRuntime.Runtime.IsCoreCLR" Value="false" Trim="true" Condition="'$(UseMonoRuntime)' == 'true'" />
906915
<RuntimeHostConfigurationOption Include="ObjCRuntime.Class.ValidateObjectPointers" Value="$(_ValidateObjectPointers)" Trim="true" />
916+
<RuntimeHostConfigurationOption Include="ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls" Value="$(_CheckForIllegalCrossThreadCalls)" Trim="true" />
907917

908918
<!-- Configure System.Net.Http Native Handlers -->
909919
<RuntimeHostConfigurationOption Include="System.Net.Http.NativeHandler.UseNSUrlSessionHandler" Value="$(_IsNSUrlSessionHandlerFeature)" Trim="true" />

src/AppKit/NSApplication.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,8 @@
3232

3333
namespace AppKit {
3434
public partial class NSApplication : NSResponder {
35-
/// <summary>To be added.</summary>
36-
/// <remarks>To be added.</remarks>
37-
public static bool CheckForIllegalCrossThreadCalls = true;
35+
/// <inheritdoc cref="Runtime.CheckForIllegalCrossThreadCalls" />
36+
public static bool CheckForIllegalCrossThreadCalls;
3837
/// <summary>To be added.</summary>
3938
/// <remarks>To be added.</remarks>
4039
public static bool CheckForEventAndDelegateMismatches = true;
@@ -53,6 +52,12 @@ public static void Init ()
5352

5453
internal static void InitializeApplication ()
5554
{
55+
// The linker replaces the 'Runtime.CheckForIllegalCrossThreadCalls' getter with a constant value, so when the UI
56+
// thread checks are disabled the assignment below (and the 'CheckForIllegalCrossThreadCalls' field
57+
// itself, unless something else references it) is trimmed away.
58+
if (Runtime.CheckForIllegalCrossThreadCalls)
59+
CheckForIllegalCrossThreadCalls = true;
60+
5661
SynchronizationContext.SetSynchronizationContext (new AppKitSynchronizationContext ());
5762
}
5863

src/ILLink.Substitutions.MacCatalyst.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
<method signature="System.Boolean GetIsARM64CallingConvention()" body="stub" feature="ObjCRuntime.Runtime.IsARM64CallingConvention" featurevalue="true" value="true" />
1717
<method signature="System.Boolean get_DynamicRegistrationSupported()" body="stub" feature="ObjCRuntime.Runtime.DynamicRegistrationSupported" featurevalue="false" value="false" />
1818
<method signature="System.Boolean get_DynamicRegistrationSupported()" body="stub" feature="ObjCRuntime.Runtime.DynamicRegistrationSupported" featurevalue="true" value="true" />
19+
<method signature="System.Boolean get_CheckForIllegalCrossThreadCalls()" body="stub" feature="ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls" featurevalue="false" value="false" />
20+
<method signature="System.Boolean get_CheckForIllegalCrossThreadCalls()" body="stub" feature="ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls" featurevalue="true" value="true" />
1921
<method signature="System.Boolean get_IsTrimmableStaticRegistrar()" body="stub" feature="ObjCRuntime.Runtime.IsTrimmableStaticRegistrar" featurevalue="false" value="false" />
2022
<method signature="System.Boolean get_IsTrimmableStaticRegistrar()" body="stub" feature="ObjCRuntime.Runtime.IsTrimmableStaticRegistrar" featurevalue="true" value="true" />
2123
<method signature="System.Boolean get_UseCFNetworkHandler()" body="stub" feature="System.Net.Http.NativeHandler.UseCFNetworkHandler" featurevalue="false" value="false" />
@@ -28,6 +30,7 @@
2830
<method signature="System.Boolean get_ValidateObjectPointers()" body="stub" feature="ObjCRuntime.Class.ValidateObjectPointers" featurevalue="true" value="true" />
2931
</type>
3032
<type fullname="UIKit.UIApplication">
33+
<method signature="System.Void EnsureUIThread()" body="stub" feature="ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls" featurevalue="false" />
3134
<method signature="System.Void EnsureEventAndDelegateAreNotMismatched(System.Object,System.Type)" body="stub" feature="System.Diagnostics.Debugger.IsSupported" featurevalue="false" />
3235
<method signature="System.Void EnsureDelegateAssignIsNotOverwritingInternalDelegate(System.Object,System.Object,System.Type)" body="stub" feature="System.Diagnostics.Debugger.IsSupported" featurevalue="false" />
3336
</type>

src/ILLink.Substitutions.iOS.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<method signature="System.Boolean GetIsARM64CallingConvention()" body="stub" feature="ObjCRuntime.Runtime.IsARM64CallingConvention" featurevalue="true" value="true" />
1919
<method signature="System.Boolean get_DynamicRegistrationSupported()" body="stub" feature="ObjCRuntime.Runtime.DynamicRegistrationSupported" featurevalue="false" value="false" />
2020
<method signature="System.Boolean get_DynamicRegistrationSupported()" body="stub" feature="ObjCRuntime.Runtime.DynamicRegistrationSupported" featurevalue="true" value="true" />
21+
<method signature="System.Boolean get_CheckForIllegalCrossThreadCalls()" body="stub" feature="ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls" featurevalue="false" value="false" />
22+
<method signature="System.Boolean get_CheckForIllegalCrossThreadCalls()" body="stub" feature="ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls" featurevalue="true" value="true" />
2123
<method signature="System.Boolean get_IsTrimmableStaticRegistrar()" body="stub" feature="ObjCRuntime.Runtime.IsTrimmableStaticRegistrar" featurevalue="false" value="false" />
2224
<method signature="System.Boolean get_IsTrimmableStaticRegistrar()" body="stub" feature="ObjCRuntime.Runtime.IsTrimmableStaticRegistrar" featurevalue="true" value="true" />
2325
<method signature="System.Boolean get_UseCFNetworkHandler()" body="stub" feature="System.Net.Http.NativeHandler.UseCFNetworkHandler" featurevalue="false" value="false" />
@@ -45,6 +47,7 @@
4547
<method signature="System.Boolean get_ValidateObjectPointers()" body="stub" feature="ObjCRuntime.Class.ValidateObjectPointers" featurevalue="true" value="true" />
4648
</type>
4749
<type fullname="UIKit.UIApplication">
50+
<method signature="System.Void EnsureUIThread()" body="stub" feature="ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls" featurevalue="false" />
4851
<method signature="System.Void EnsureEventAndDelegateAreNotMismatched(System.Object,System.Type)" body="stub" feature="System.Diagnostics.Debugger.IsSupported" featurevalue="false" />
4952
<method signature="System.Void EnsureDelegateAssignIsNotOverwritingInternalDelegate(System.Object,System.Object,System.Type)" body="stub" feature="System.Diagnostics.Debugger.IsSupported" featurevalue="false" />
5053
</type>

src/ILLink.Substitutions.macOS.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
<method signature="System.Boolean GetIsARM64CallingConvention()" body="stub" feature="ObjCRuntime.Runtime.IsARM64CallingConvention" featurevalue="true" value="true" />
1616
<method signature="System.Boolean get_DynamicRegistrationSupported()" body="stub" feature="ObjCRuntime.Runtime.DynamicRegistrationSupported" featurevalue="false" value="false" />
1717
<method signature="System.Boolean get_DynamicRegistrationSupported()" body="stub" feature="ObjCRuntime.Runtime.DynamicRegistrationSupported" featurevalue="true" value="true" />
18+
<method signature="System.Boolean get_CheckForIllegalCrossThreadCalls()" body="stub" feature="ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls" featurevalue="false" value="false" />
19+
<method signature="System.Boolean get_CheckForIllegalCrossThreadCalls()" body="stub" feature="ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls" featurevalue="true" value="true" />
1820
<method signature="System.Boolean get_IsTrimmableStaticRegistrar()" body="stub" feature="ObjCRuntime.Runtime.IsTrimmableStaticRegistrar" featurevalue="false" value="false" />
1921
<method signature="System.Boolean get_IsTrimmableStaticRegistrar()" body="stub" feature="ObjCRuntime.Runtime.IsTrimmableStaticRegistrar" featurevalue="true" value="true" />
2022
<method signature="System.Boolean get_UseCFNetworkHandler()" body="stub" feature="System.Net.Http.NativeHandler.UseCFNetworkHandler" featurevalue="false" value="false" />
@@ -27,6 +29,7 @@
2729
<method signature="System.Boolean get_ValidateObjectPointers()" body="stub" feature="ObjCRuntime.Class.ValidateObjectPointers" featurevalue="true" value="true" />
2830
</type>
2931
<type fullname="AppKit.NSApplication">
32+
<method signature="System.Void EnsureUIThread()" body="stub" feature="ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls" featurevalue="false" />
3033
<method signature="System.Void EnsureEventAndDelegateAreNotMismatched(System.Object,System.Type)" body="stub" feature="System.Diagnostics.Debugger.IsSupported" featurevalue="false" />
3134
<method signature="System.Void EnsureDelegateAssignIsNotOverwritingInternalDelegate(System.Object,System.Object,System.Type)" body="stub" feature="System.Diagnostics.Debugger.IsSupported" featurevalue="false" />
3235
</type>

src/ILLink.Substitutions.tvOS.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<method signature="System.Boolean GetIsARM64CallingConvention()" body="stub" feature="ObjCRuntime.Runtime.IsARM64CallingConvention" featurevalue="true" value="true" />
1919
<method signature="System.Boolean get_DynamicRegistrationSupported()" body="stub" feature="ObjCRuntime.Runtime.DynamicRegistrationSupported" featurevalue="false" value="false" />
2020
<method signature="System.Boolean get_DynamicRegistrationSupported()" body="stub" feature="ObjCRuntime.Runtime.DynamicRegistrationSupported" featurevalue="true" value="true" />
21+
<method signature="System.Boolean get_CheckForIllegalCrossThreadCalls()" body="stub" feature="ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls" featurevalue="false" value="false" />
22+
<method signature="System.Boolean get_CheckForIllegalCrossThreadCalls()" body="stub" feature="ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls" featurevalue="true" value="true" />
2123
<method signature="System.Boolean get_IsTrimmableStaticRegistrar()" body="stub" feature="ObjCRuntime.Runtime.IsTrimmableStaticRegistrar" featurevalue="false" value="false" />
2224
<method signature="System.Boolean get_IsTrimmableStaticRegistrar()" body="stub" feature="ObjCRuntime.Runtime.IsTrimmableStaticRegistrar" featurevalue="true" value="true" />
2325
<method signature="System.Boolean get_UseCFNetworkHandler()" body="stub" feature="System.Net.Http.NativeHandler.UseCFNetworkHandler" featurevalue="false" value="false" />
@@ -45,6 +47,7 @@
4547
<method signature="System.Boolean get_ValidateObjectPointers()" body="stub" feature="ObjCRuntime.Class.ValidateObjectPointers" featurevalue="true" value="true" />
4648
</type>
4749
<type fullname="UIKit.UIApplication">
50+
<method signature="System.Void EnsureUIThread()" body="stub" feature="ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls" featurevalue="false" />
4851
<method signature="System.Void EnsureEventAndDelegateAreNotMismatched(System.Object,System.Type)" body="stub" feature="System.Diagnostics.Debugger.IsSupported" featurevalue="false" />
4952
<method signature="System.Void EnsureDelegateAssignIsNotOverwritingInternalDelegate(System.Object,System.Object,System.Type)" body="stub" feature="System.Diagnostics.Debugger.IsSupported" featurevalue="false" />
5053
</type>

src/ObjCRuntime/Runtime.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,26 @@ public static bool DynamicRegistrationSupported {
290290
[BindingImpl (BindingImplOptions.Optimizable)]
291291
internal static bool UseCFNetworkHandler => AppContext.TryGetSwitch ("System.Net.Http.NativeHandler.UseCFNetworkHandler", out bool isDefault) ? isDefault : false;
292292

293+
// The linker may turn calls to this property into a constant
294+
/// <summary>Determines whether the debug builds will enforce that calls done to AppKit/UIKit APIs are only issued from the UI thread.</summary>
295+
/// <remarks>
296+
/// <para>
297+
/// On debug builds, the runtime will enforce that calls made to
298+
/// AppKit/UIKit APIs are only done from the main thread. This is
299+
/// useful to spot code that could inadvertently use AppKit/UIKit from
300+
/// a non-UI thread which can corrupt state and could lead to
301+
/// very hard to debug problems.
302+
/// </para>
303+
/// <para>
304+
/// But sometimes it might be useful to disable this check,
305+
/// either because you can ensure that AppKit/UIKit is not in use at
306+
/// this point or because the APIs in question might have later been
307+
/// relaxed or made thread safe by Apple.
308+
/// </para>
309+
/// </remarks>
310+
[BindingImpl (BindingImplOptions.Optimizable)]
311+
internal static bool CheckForIllegalCrossThreadCalls => AppContext.TryGetSwitch ("ObjCRuntime.Runtime.CheckForIllegalCrossThreadCalls", out bool enabled) ? enabled : true;
312+
293313
internal static bool Initialized {
294314
get { return initialized; }
295315
}

src/UIKit/UIApplication.cs

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,8 @@ public UIKitThreadAccessException () : base ("UIKit Consistency error: you are c
2626

2727
public partial class UIApplication
2828
: UIResponder {
29-
/// <summary>Determines whether the debug builds of MonoTouch will enforce that calls done to UIKit are only issued from the UI thread.</summary>
30-
/// <remarks>
31-
/// <para>
32-
/// On debug builds, MonoTouch will enforce that calls made to
33-
/// UIKit APIs are only done from the UIKit thread. This is
34-
/// useful to spot code that could inadvertently use UIKit from
35-
/// a non-UI thread which can corrupt the UIKit state and could
36-
/// lead to very hard to debug problems.
37-
/// </para>
38-
/// <para>
39-
/// But sometimes it might be useful to disable this check,
40-
/// either because you can ensure that UIKit is not in use at
41-
/// this point or because MonoTouch might be enforcing the
42-
/// checks in APIs that might have later been relaxed or made
43-
/// thread safe by iOS.
44-
///
45-
/// </para>
46-
/// </remarks>
47-
public static bool CheckForIllegalCrossThreadCalls = true;
29+
/// <inheritdoc cref="Runtime.CheckForIllegalCrossThreadCalls" />
30+
public static bool CheckForIllegalCrossThreadCalls;
4831
/// <summary>If <see langword="true" />, the system will try to diagnose potential mistakes where events and delegate-object overrides are in conflict.</summary>
4932
public static bool CheckForEventAndDelegateMismatches = true;
5033

@@ -70,6 +53,12 @@ static int UIApplicationMain (int argc, /* char[]* */ string []? argv, /* NSStri
7053
// NOTE: must be called from the main thread, e.g. for extensions
7154
internal static void InitializeApplication ()
7255
{
56+
// The linker replaces the 'Runtime.CheckForIllegalCrossThreadCalls' getter with a constant value, so when the UI
57+
// thread checks are disabled the assignment below (and the 'CheckForIllegalCrossThreadCalls' field
58+
// itself, unless something else references it) is trimmed away.
59+
if (Runtime.CheckForIllegalCrossThreadCalls)
60+
CheckForIllegalCrossThreadCalls = true;
61+
7362
SynchronizationContext.SetSynchronizationContext (new UIKitSynchronizationContext ());
7463
}
7564

0 commit comments

Comments
 (0)