Skip to content

Add MainThread.SetCustomImplementation for custom platform backends#34230

Open
Redth wants to merge 1 commit intomainfrom
dev/redth/fix-34101
Open

Add MainThread.SetCustomImplementation for custom platform backends#34230
Redth wants to merge 1 commit intomainfrom
dev/redth/fix-34101

Conversation

@Redth
Copy link
Member

@Redth Redth commented Feb 25, 2026

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() and MainThread.InvokeOnMainThreadAsync() throw NotImplementedInReferenceAssemblyException because 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 own IsMainThread and BeginInvokeOnMainThread implementations.

Changes

  • MainThread.shared.cs: Added static callback fields and SetCustomImplementation(Func<bool>, Action<Action>) public method
  • MainThread.netstandard.cs: Modified to check for custom callbacks before throwing NotImplementedInReferenceAssemblyException
  • PublicAPI.Unshipped.txt: Added new public API entry for all targets
  • MainThread_Tests.cs: Added 6 unit tests covering the new functionality

Usage

Custom platform backends can register their implementation during startup:

MainThread.SetCustomImplementation(
    isMainThread: () => /* check if on UI thread */,
    beginInvokeOnMainThread: action => /* dispatch to UI thread */);

For example, a GTK backend might use:

MainThread.SetCustomImplementation(
    isMainThread: () => GLib.MainContext.Default.IsOwner,
    beginInvokeOnMainThread: action => GLib.Idle.Add(() => { action(); return false; }));

On standard platforms (iOS, Android, Windows, MacCatalyst), the existing platform-specific implementations continue to be used unchanged.

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>
Copilot AI review requested due to automatic review settings February 25, 2026 02:01
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 MainThread implementation 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
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

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 !).

Suggested change
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

Copilot uses AI. Check for mistakes.
@@ -1 +1,2 @@
#nullable enable
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

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 !).

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +25
s_isMainThreadImplementation = isMainThread ?? throw new ArgumentNullException(nameof(isMainThread));
s_beginInvokeOnMainThreadImplementation = beginInvokeOnMainThread ?? throw new ArgumentNullException(nameof(beginInvokeOnMainThread));
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
Comment on lines +69 to +76
// 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);
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
// 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();

Copilot uses AI. Check for mistakes.
@@ -1 +1,2 @@
#nullable enable
#nullable enable
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
#nullable enable
#nullable enable

Copilot uses AI. Check for mistakes.
@@ -1 +1,2 @@
#nullable enable
#nullable enable
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
#nullable enable
#nullable enable

Copilot uses AI. Check for mistakes.
@@ -1 +1,2 @@
#nullable enable
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

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 !).

Suggested change
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

Copilot uses AI. Check for mistakes.
@@ -1 +1,2 @@
#nullable enable
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

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 !).

Suggested change
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

Copilot uses AI. Check for mistakes.
@@ -1 +1,2 @@
#nullable enable
static Microsoft.Maui.ApplicationModel.MainThread.SetCustomImplementation(System.Func<bool>! isMainThread, System.Action<System.Action!>! beginInvokeOnMainThread) -> void
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

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 !).

Suggested change
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

Copilot uses AI. Check for mistakes.
/// </summary>
public static partial class MainThread
{
static Func<bool> s_isMainThreadImplementation;
Copy link
Member

Choose a reason for hiding this comment

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

do we use s_ ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants