// MIT License - Copyright (c) 2025 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.Helper
{
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
///
/// Captures Unity’s main-thread context and exposes guard helpers for APIs that must run on that thread.
/// This prevents accidental background-thread access to Unity APIs.
///
/// Typical usage inside getters or event handlers:
///
/// public T Instance
/// {
/// get
/// {
/// UnityMainThreadGuard.EnsureMainThread();
/// return _instance;
/// }
/// }
///
/// public void RefreshUI()
/// {
/// UnityMainThreadGuard.EnsureMainThread("Refreshing UI");
/// // safe to interact with Unity objects here
/// }
///
///
///
internal static class UnityMainThreadGuard
{
private static int _mainThreadId;
private static SynchronizationContext _mainThreadContext;
private static int _initialized;
internal static bool IsInitialized => _initialized == 1;
internal static SynchronizationContext MainThreadContext => _mainThreadContext;
internal static bool IsMainThread
{
get
{
if (!IsInitialized)
{
return true;
}
if (_mainThreadId == 0)
{
return true;
}
int currentId = Thread.CurrentThread.ManagedThreadId;
return currentId == _mainThreadId;
}
}
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void CaptureRuntimeThread()
{
Capture(Thread.CurrentThread);
}
#if UNITY_EDITOR
[InitializeOnLoadMethod]
internal static void CaptureEditorThread()
{
if (Application.isPlaying)
{
return;
}
Capture(Thread.CurrentThread);
}
#endif
///
/// Captures the provided thread as the main thread and stores its .
/// Normally invoked automatically via /
/// .
///
/// Thread to treat as the Unity main thread.
internal static void Capture(Thread thread)
{
if (thread == null)
{
return;
}
_mainThreadId = thread.ManagedThreadId;
_mainThreadContext = SynchronizationContext.Current ?? new SynchronizationContext();
Interlocked.Exchange(ref _initialized, 1);
}
///
/// Throws an when invoked on a non-main thread.
/// Caller metadata is captured automatically via compiler attributes so the resulting message
/// pinpoints the offending member and source location.
///
/// Example:
///
/// void Update()
/// {
/// UnityMainThreadGuard.EnsureMainThread();
/// // Update logic...
/// }
///
///
///
///
/// Optional label describing why the guard is required, appended to the error message.
///
/// Populated automatically with .
/// Populated automatically with .
/// Populated automatically with .
internal static void EnsureMainThread(
string context = null,
[CallerMemberName] string memberName = null,
[CallerFilePath] string callerFilePath = null,
[CallerLineNumber] int callerLineNumber = 0
)
{
if (IsMainThread)
{
return;
}
string fileBaseName = string.IsNullOrWhiteSpace(callerFilePath)
? "UnknownFile"
: Path.GetFileNameWithoutExtension(callerFilePath);
string fileLabel = string.IsNullOrWhiteSpace(callerFilePath)
? "UnknownFile"
: Path.GetFileName(callerFilePath);
string location = string.IsNullOrEmpty(memberName)
? fileBaseName
: $"{fileBaseName}.{memberName}";
if (!string.IsNullOrEmpty(context))
{
location = $"{location} ({context})";
}
string message =
$"{location} must be accessed on Unity's main thread (called from {fileLabel}:{callerLineNumber}). Use UnityMainThreadDispatcher.Instance.RunOnMainThread to marshal work safely.";
throw new InvalidOperationException(message);
}
internal static bool TryPostToMainThread(Action action)
{
if (action == null)
{
return false;
}
SynchronizationContext context = _mainThreadContext;
if (context == null)
{
return false;
}
try
{
context.Post(static state => ((Action)state)?.Invoke(), action);
return true;
}
catch
{
return false;
}
}
}
}