// MIT License // Copyright(c) 2016 Modest Tree Media Inc // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.ExceptionServices; using System.Text; using System.Threading; using UnityEngine; using Object = UnityEngine.Object; namespace Microsoft.MixedReality.Toolkit.Utilities { /// /// We could just add a generic GetAwaiter to YieldInstruction and CustomYieldInstruction /// but instead we add specific methods to each derived class to allow for return values /// that make the most sense for the specific instruction type. /// public static class AwaiterExtensions { public static SimpleCoroutineAwaiter GetAwaiter(this WaitForSeconds instruction) { return GetAwaiterReturnVoid(instruction); } public static SimpleCoroutineAwaiter GetAwaiter(this WaitForUpdate instruction) { return GetAwaiterReturnVoid(instruction); } public static SimpleCoroutineAwaiter GetAwaiter(this WaitForEndOfFrame instruction) { return GetAwaiterReturnVoid(instruction); } public static SimpleCoroutineAwaiter GetAwaiter(this WaitForFixedUpdate instruction) { return GetAwaiterReturnVoid(instruction); } public static SimpleCoroutineAwaiter GetAwaiter(this WaitForSecondsRealtime instruction) { return GetAwaiterReturnVoid(instruction); } public static SimpleCoroutineAwaiter GetAwaiter(this WaitUntil instruction) { return GetAwaiterReturnVoid(instruction); } public static SimpleCoroutineAwaiter GetAwaiter(this WaitWhile instruction) { return GetAwaiterReturnVoid(instruction); } #if !UNITY_2023_1_OR_NEWER public static SimpleCoroutineAwaiter GetAwaiter(this AsyncOperation instruction) { return GetAwaiterReturnSelf(instruction); } public static SimpleCoroutineAwaiter GetAwaiter(this ResourceRequest instruction) { var awaiter = new SimpleCoroutineAwaiter(); RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( InstructionWrappers.ResourceRequest(awaiter, instruction))); return awaiter; } public static SimpleCoroutineAwaiter GetAwaiter(this AssetBundleCreateRequest instruction) { var awaiter = new SimpleCoroutineAwaiter(); RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( InstructionWrappers.AssetBundleCreateRequest(awaiter, instruction))); return awaiter; } public static SimpleCoroutineAwaiter GetAwaiter(this AssetBundleRequest instruction) { var awaiter = new SimpleCoroutineAwaiter(); RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( InstructionWrappers.AssetBundleRequest(awaiter, instruction))); return awaiter; } #endif public static SimpleCoroutineAwaiter GetAwaiter(this IEnumerator coroutine) { var awaiter = new SimpleCoroutineAwaiter(); RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( new CoroutineWrapper(coroutine, awaiter).Run())); return awaiter; } public static SimpleCoroutineAwaiter GetAwaiter(this IEnumerator coroutine) { var awaiter = new SimpleCoroutineAwaiter(); RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( new CoroutineWrapper(coroutine, awaiter).Run())); return awaiter; } private static SimpleCoroutineAwaiter GetAwaiterReturnVoid(object instruction) { var awaiter = new SimpleCoroutineAwaiter(); RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( InstructionWrappers.ReturnVoid(awaiter, instruction))); return awaiter; } private static SimpleCoroutineAwaiter GetAwaiterReturnSelf(T instruction) { var awaiter = new SimpleCoroutineAwaiter(); RunOnUnityScheduler(() => AsyncCoroutineRunner.Instance.StartCoroutine( InstructionWrappers.ReturnSelf(awaiter, instruction))); return awaiter; } private static void RunOnUnityScheduler(Action action) { if (SynchronizationContext.Current == SyncContextUtility.UnitySynchronizationContext) { action(); } else { // Make sure there is a running instance of AsyncCoroutineRunner before calling AsyncCoroutineRunner.Post // If not warn the user. Note we cannot call AsyncCoroutineRunner.Instance here as that getter contains // calls to Unity functions that can only be run on the Unity thread if (!AsyncCoroutineRunner.IsInstanceRunning) { Debug.LogWarning("There is no active AsyncCoroutineRunner when an action is posted. Place a GameObject " + "at the root of the scene and attach the AsyncCoroutineRunner script to make it function properly."); } AsyncCoroutineRunner.Post(action); } } /// /// Processes Coroutine and notifies completion with result. /// /// The result type. public class SimpleCoroutineAwaiter : INotifyCompletion { private Exception exception; private Action continuation; private T result; public bool IsCompleted { get; private set; } public T GetResult() { Debug.Assert(IsCompleted); if (exception != null) { ExceptionDispatchInfo.Capture(exception).Throw(); } return result; } public void Complete(T taskResult, Exception e) { Debug.Assert(!IsCompleted); IsCompleted = true; exception = e; result = taskResult; // Always trigger the continuation on the unity thread // when awaiting on unity yield instructions. if (continuation != null) { RunOnUnityScheduler(continuation); } } void INotifyCompletion.OnCompleted(Action notifyContinuation) { Debug.Assert(continuation == null); Debug.Assert(!IsCompleted); continuation = notifyContinuation; } } /// /// Processes Coroutine and notifies completion. /// public class SimpleCoroutineAwaiter : INotifyCompletion { private Exception exception; private Action continuation; public bool IsCompleted { get; private set; } public void GetResult() { Debug.Assert(IsCompleted); if (exception != null) { ExceptionDispatchInfo.Capture(exception).Throw(); } } public void Complete(Exception e) { Debug.Assert(!IsCompleted); IsCompleted = true; exception = e; // Always trigger the continuation on the unity thread // when awaiting on unity yield instructions. if (continuation != null) { RunOnUnityScheduler(continuation); } } void INotifyCompletion.OnCompleted(Action notifyContinuation) { Debug.Assert(continuation == null); Debug.Assert(!IsCompleted); continuation = notifyContinuation; } } private class CoroutineWrapper { private readonly SimpleCoroutineAwaiter awaiter; private readonly Stack processStack; public CoroutineWrapper(IEnumerator coroutine, SimpleCoroutineAwaiter awaiter) { processStack = new Stack(); processStack.Push(coroutine); this.awaiter = awaiter; } public IEnumerator Run() { while (true) { var topWorker = processStack.Peek(); bool isDone; try { isDone = !topWorker.MoveNext(); } catch (Exception e) { // The IEnumerators we have in the process stack do not tell us the // actual names of the coroutine methods but it does tell us the objects // that the IEnumerators are associated with, so we can at least try // adding that to the exception output var objectTrace = GenerateObjectTrace(processStack); awaiter.Complete(default(T), objectTrace.Any() ? new Exception(GenerateObjectTraceMessage(objectTrace), e) : e); yield break; } if (isDone) { processStack.Pop(); if (processStack.Count == 0) { awaiter.Complete((T)topWorker.Current, null); yield break; } } // We could just yield return nested IEnumerator's here but we choose to do // our own handling here so that we can catch exceptions in nested coroutines // instead of just top level coroutine if (topWorker.Current is IEnumerator item) { processStack.Push(item); } else { // Return the current value to the unity engine so it can handle things like // WaitForSeconds, WaitToEndOfFrame, etc. yield return topWorker.Current; } } } private static string GenerateObjectTraceMessage(List objTrace) { var result = new StringBuilder(); foreach (var objType in objTrace) { if (result.Length != 0) { result.Append(" -> "); } result.Append(objType); } result.AppendLine(); return $"Unity Coroutine Object Trace: {result}"; } private static List GenerateObjectTrace(IEnumerable enumerators) { var objTrace = new List(); foreach (var enumerator in enumerators) { // NOTE: This only works with scripting engine 4.6 // And could easily stop working with unity updates var field = enumerator.GetType().GetField("$this", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance); if (field == null) { continue; } var obj = field.GetValue(enumerator); if (obj == null) { continue; } var objType = obj.GetType(); if (!objTrace.Any() || objType != objTrace.Last()) { objTrace.Add(objType); } } objTrace.Reverse(); return objTrace; } } private static class InstructionWrappers { public static IEnumerator ReturnVoid(SimpleCoroutineAwaiter awaiter, object instruction) { // For simple instructions we assume that they don't throw exceptions yield return instruction; awaiter.Complete(null); } public static IEnumerator AssetBundleCreateRequest(SimpleCoroutineAwaiter awaiter, AssetBundleCreateRequest instruction) { yield return instruction; awaiter.Complete(instruction.assetBundle, null); } public static IEnumerator ReturnSelf(SimpleCoroutineAwaiter awaiter, T instruction) { yield return instruction; awaiter.Complete(instruction, null); } public static IEnumerator AssetBundleRequest(SimpleCoroutineAwaiter awaiter, AssetBundleRequest instruction) { yield return instruction; awaiter.Complete(instruction.asset, null); } public static IEnumerator ResourceRequest(SimpleCoroutineAwaiter awaiter, ResourceRequest instruction) { yield return instruction; awaiter.Complete(instruction.asset, null); } } } }