Add MainThread.SetCustomImplementation for custom platform backends#34230
Add MainThread.SetCustomImplementation for custom platform backends#34230
Conversation
Fixes #34101. On custom platform backends (e.g. Linux/GTK), MainThread throws NotImplementedInReferenceAssemblyException because there is no platform-specific partial class implementation. This adds a public SetCustomImplementation method that allows custom backends to provide their own IsMainThread and BeginInvokeOnMainThread implementations. Changes: - Add SetCustomImplementation(Func<bool>, Action<Action>) to MainThread - Modify MainThread.netstandard.cs to use custom callbacks before throwing - Add unit tests verifying the new functionality - Update PublicAPI.Unshipped.txt for all targets Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Adds an opt-in mechanism for custom platform backends (e.g., Linux/GTK) to provide MainThread behavior when the portable (*.netstandard.cs) implementation would otherwise throw, addressing the “custom backend throws NotImplementedInReferenceAssemblyException” scenario.
Changes:
- Added
MainThread.SetCustomImplementation(Func<bool>, Action<Action>)and backing delegate fields. - Updated the netstandard
MainThreadimplementation to use the custom delegates when present. - Added PublicAPI entries across TFMs and introduced new unit tests for the custom-implementation behavior.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Essentials/src/MainThread/MainThread.shared.cs | Adds public API + stores delegates for custom main-thread detection/dispatch. |
| src/Essentials/src/MainThread/MainThread.netstandard.cs | Uses custom delegates before throwing the reference-assembly exception. |
| src/Essentials/src/PublicAPI/netstandard/PublicAPI.Unshipped.txt | Adds new API entry (but has BOM/format concerns). |
| src/Essentials/src/PublicAPI/net/PublicAPI.Unshipped.txt | Adds new API entry (but has BOM/format concerns). |
| src/Essentials/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt | Adds new API entry (format appears inconsistent with shipped conventions). |
| src/Essentials/src/PublicAPI/net-tizen/PublicAPI.Unshipped.txt | Adds new API entry (format appears inconsistent with shipped conventions). |
| src/Essentials/src/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt | Adds new API entry (format appears inconsistent with shipped conventions). |
| src/Essentials/src/PublicAPI/net-ios/PublicAPI.Unshipped.txt | Adds new API entry (format appears inconsistent with shipped conventions). |
| src/Essentials/src/PublicAPI/net-android/PublicAPI.Unshipped.txt | Adds new API entry (format appears inconsistent with shipped conventions). |
| src/Essentials/test/UnitTests/MainThread_Tests.cs | Adds unit tests validating custom-implementation behavior and argument validation. |
| @@ -1 +1,2 @@ | |||
| #nullable enable | |||
| static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void | |||
There was a problem hiding this comment.
PublicAPI formatting here is inconsistent with the rest of this TFM's shipped APIs (which use the ~static prefix and omit nullable ! annotations for MainThread members). As written, this entry likely won't match the API as generated from the current (nullable-oblivious) source. Update the entry to the correct PublicAPI format for this assembly (likely ~static ... without !).
| static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void | |
| ~static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool> isMainThread, System.Action<System.Action> beginInvokeOnMainThread) -> void |
| @@ -1 +1,2 @@ | |||
| #nullable enable | |||
| static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void | |||
There was a problem hiding this comment.
PublicAPI formatting here is inconsistent with the rest of this TFM's shipped APIs (which use the ~static prefix and omit nullable ! annotations for MainThread members). As written, this entry likely won't match the API as generated from the current (nullable-oblivious) source. Update the entry to the correct PublicAPI format for this assembly (likely ~static ... without !).
| static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void | |
| ~static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool> isMainThread, System.Action<System.Action> beginInvokeOnMainThread) -> void |
| s_isMainThreadImplementation = isMainThread ?? throw new ArgumentNullException(nameof(isMainThread)); | ||
| s_beginInvokeOnMainThreadImplementation = beginInvokeOnMainThread ?? throw new ArgumentNullException(nameof(beginInvokeOnMainThread)); |
There was a problem hiding this comment.
SetCustomImplementation mutates s_isMainThreadImplementation before validating beginInvokeOnMainThread. If beginInvokeOnMainThread is null, the method throws but leaves the class in a partially-updated state (IsMainThread may start returning a value instead of throwing). Validate both parameters first (or assign atomically) so the method is exception-safe and does not change state on failure.
| s_isMainThreadImplementation = isMainThread ?? throw new ArgumentNullException(nameof(isMainThread)); | |
| s_beginInvokeOnMainThreadImplementation = beginInvokeOnMainThread ?? throw new ArgumentNullException(nameof(beginInvokeOnMainThread)); | |
| if (isMainThread is null) | |
| throw new ArgumentNullException(nameof(isMainThread)); | |
| if (beginInvokeOnMainThread is null) | |
| throw new ArgumentNullException(nameof(beginInvokeOnMainThread)); | |
| s_isMainThreadImplementation = isMainThread; | |
| s_beginInvokeOnMainThreadImplementation = beginInvokeOnMainThread; |
| // Use reflection to clear the static fields since there's no public reset API | ||
| var type = typeof(MainThread); | ||
| var isMainThreadField = type.GetField("s_isMainThreadImplementation", | ||
| System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); | ||
| var beginInvokeField = type.GetField("s_beginInvokeOnMainThreadImplementation", | ||
| System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); | ||
| isMainThreadField?.SetValue(null, null); | ||
| beginInvokeField?.SetValue(null, null); |
There was a problem hiding this comment.
These tests reset MainThread state by reflecting on private field names. This is brittle (renames/ILLink changes can break tests) and bypasses InternalsVisibleTo that the Essentials assembly already provides. Consider adding an internal reset helper on MainThread (accessible to Microsoft.Maui.Essentials.UnitTests) and call that instead of reflection.
| // Use reflection to clear the static fields since there's no public reset API | |
| var type = typeof(MainThread); | |
| var isMainThreadField = type.GetField("s_isMainThreadImplementation", | |
| System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); | |
| var beginInvokeField = type.GetField("s_beginInvokeOnMainThreadImplementation", | |
| System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic); | |
| isMainThreadField?.SetValue(null, null); | |
| beginInvokeField?.SetValue(null, null); | |
| // Use the internal reset helper on MainThread instead of reflection on private fields | |
| MainThread.Reset(); |
| @@ -1 +1,2 @@ | |||
| #nullable enable | |||
| #nullable enable | |||
There was a problem hiding this comment.
The first line now contains a BOM character before #nullable enable (shows up as �#nullable enable). This frequently causes PublicAPI tooling diffs/failures. Please remove the BOM so the line is exactly #nullable enable.
| #nullable enable | |
| #nullable enable |
| @@ -1 +1,2 @@ | |||
| #nullable enable | |||
| #nullable enable | |||
There was a problem hiding this comment.
The first line now contains a BOM character before #nullable enable. This frequently causes PublicAPI tooling diffs/failures. Please remove the BOM so the line is exactly #nullable enable.
| #nullable enable | |
| #nullable enable |
| @@ -1 +1,2 @@ | |||
| #nullable enable | |||
| static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void | |||
There was a problem hiding this comment.
PublicAPI formatting here is inconsistent with the rest of this TFM's shipped APIs (which use the ~static prefix and omit nullable ! annotations for MainThread members). As written, this entry likely won't match the API as generated from the current (nullable-oblivious) source. Update the entry to the correct PublicAPI format for this assembly (likely ~static ... without !).
| static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void | |
| ~static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool> isMainThread, System.Action<System.Action> beginInvokeOnMainThread) -> void |
| @@ -1 +1,2 @@ | |||
| #nullable enable | |||
| static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void | |||
There was a problem hiding this comment.
PublicAPI formatting here is inconsistent with the rest of this TFM's shipped APIs (which use the ~static prefix and omit nullable ! annotations for MainThread members). As written, this entry likely won't match the API as generated from the current (nullable-oblivious) source. Update the entry to the correct PublicAPI format for this assembly (likely ~static ... without !).
| static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void | |
| ~static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool> isMainThread, System.Action<System.Action> beginInvokeOnMainThread) -> void |
| @@ -1 +1,2 @@ | |||
| #nullable enable | |||
| static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void | |||
There was a problem hiding this comment.
PublicAPI formatting here is inconsistent with the rest of this TFM's shipped APIs (which use the ~static prefix and omit nullable ! annotations for MainThread members). As written, this entry likely won't match the API as generated from the current (nullable-oblivious) source. Update the entry to the correct PublicAPI format for this assembly (likely ~static ... without !).
| static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void | |
| ~static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool> isMainThread, System.Action<System.Action> beginInvokeOnMainThread) -> void |
| /// </summary> | ||
| public static partial class MainThread | ||
| { | ||
| static Func<bool> s_isMainThreadImplementation; |
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Description
Fixes #34101
On custom platform backends (e.g. Linux/GTK),
MainThread.BeginInvokeOnMainThread()andMainThread.InvokeOnMainThreadAsync()throwNotImplementedInReferenceAssemblyExceptionbecause there is no platform-specific partial class implementation for the netstandard TFM.This PR adds a public
MainThread.SetCustomImplementation()method that allows custom backends to register their ownIsMainThreadandBeginInvokeOnMainThreadimplementations.Changes
MainThread.shared.cs: Added static callback fields andSetCustomImplementation(Func<bool>, Action<Action>)public methodMainThread.netstandard.cs: Modified to check for custom callbacks before throwingNotImplementedInReferenceAssemblyExceptionPublicAPI.Unshipped.txt: Added new public API entry for all targetsMainThread_Tests.cs: Added 6 unit tests covering the new functionalityUsage
Custom platform backends can register their implementation during startup:
For example, a GTK backend might use:
On standard platforms (iOS, Android, Windows, MacCatalyst), the existing platform-specific implementations continue to be used unchanged.