1

I am trying to show a simple image on a Unity Editor window, it worked for a while and then I guess I made some changes that broke it and I keep getting a NullReferenceException. Here is my simple example:

public class ShowImagePreview : EditorWindow
{
  private Texture2D texture;

  private async Task<Texture2D> GetHTTPTextureAsync(string imageUrl)
  {
    UnityWebRequest request = UnityWebRequestTexture.GetTexture(imageUrl);
    await request.SendWebRequest();

    if (request.isNetworkError || request.isHttpError)
    {
      Debug.Log("Something went wrong!");
      Debug.Log(request.error);
      return null;
    }

    return ((DownloadHandlerTexture)request.downloadHandler).texture;
  }

  private async Task OnGUI()
  {
    GUILayout.BeginHorizontal("box");
    if (texture != null)
    {
      GUILayout.Label("Texture is not null!");
      GUILayout.Box(texture, GUILayout.Width(140), GUILayout.Height(100));
    }
    else
    {
      GUILayout.Label("Texture is NULL!");
      try
      {
        // This is what's causing the issue and keeping the texture value to null
        texture = await GetHTTPTextureAsync("https://nikkorpon.files.wordpress.com/2011/02/random-shots-059.jpg");
      }
      catch (Exception e)
      {
        Debug.Log(e);
      }
    }
    GUILayout.EndHorizontal();
  }
}

This is an extract from a larger script. The image urls are actually dynamic but for the sake of this example I just grabbed a random image url. I am using the Async/Await plugin which works as expected with any other request. Any idea about what I might be doing wrong ?

Here is the error message in full:

System.NullReferenceException: Object reference not set to an instance of an object
  at IEnumeratorAwaitExtensions.RunOnUnityScheduler (System.Action action) [0x00042] in /Users/user_name/Projects/project_name/Assets/Plugins/AsyncAwaitUtil/Source/IEnumeratorAwaitExtensions.cs:128 
  at IEnumeratorAwaitExtensions.GetAwaiterReturnSelf[T] (T instruction) [0x00025] in /Users/user_name/Projects/project_name/Assets/Plugins/AsyncAwaitUtil/Source/IEnumeratorAwaitExtensions.cs:115 
  at IEnumeratorAwaitExtensions.GetAwaiter (UnityEngine.AsyncOperation instruction) [0x00002] in /Users/user_name/Projects/project_name/Assets/Plugins/AsyncAwaitUtil/Source/IEnumeratorAwaitExtensions.cs:55 
  at ShowImagePreview+<GetHTTPTextureAsync>c__async0.MoveNext () [0x00033] in /Users/user_name/Projects/project_name/Assets/Resources/Scripts/Import.cs:18 
--- End of stack trace from previous location where exception was thrown ---
  at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () [0x0000c] in <f2e6809acb14476a81f399aeb800f8f2>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) [0x0003e] in <f2e6809acb14476a81f399aeb800f8f2>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) [0x00028] in <f2e6809acb14476a81f399aeb800f8f2>:0 
  at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) [0x00008] in <f2e6809acb14476a81f399aeb800f8f2>:0 
  at System.Runtime.CompilerServices.TaskAwaiter`1[TResult].GetResult () [0x00000] in <f2e6809acb14476a81f399aeb800f8f2>:0 
  at ShowImagePreview+<OnGUI>c__async1.MoveNext () [0x0010b] in /Users/user_name/Projects/project_name/Assets/Resources/Scripts/Import.cs:43 
UnityEngine.Debug:Log(Object)
<OnGUI>c__async1:MoveNext() (at Assets/Resources/Scripts/Import.cs:47)
System.Runtime.CompilerServices.AsyncTaskMethodBuilder:Start(<OnGUI>c__async1&)
ShowImagePreview:OnGUI()
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr)
G4bri3l
  • 4,996
  • 4
  • 31
  • 53
  • Can you include the error message, and add a note to your code where it is erroring? – AresCaelum Aug 06 '19 at 18:33
  • Absolutely, sorry I forgot to add that! – G4bri3l Aug 06 '19 at 18:33
  • Possible duplicate of [What is a NullReferenceException, and how do I fix it?](https://stackoverflow.com/questions/4660142/what-is-a-nullreferenceexception-and-how-do-i-fix-it) – Dour High Arch Aug 06 '19 at 18:37
  • @DourHighArch I already checked that and could not find something that would help solve my problem. You can see in my code that I have an explicit null check on the texture. I will dig again in that post to double check I didn't miss anything. Also there are no reference at all to the use of async/await and how that could lead to a NullReferenceError. If there's a solution to this problem in that post please would you mind pointing me in the right direction ? – G4bri3l Aug 06 '19 at 18:40
  • It looks like an issue in `IEnumeratorAwaitExtensions`. Checking the source code (I'm assuming https://github.com/modesttree/Unity3dAsyncAwaitUtil/blob/master/UnityProject/Assets/Plugins/AsyncAwaitUtil/Source/IEnumeratorAwaitExtensions.cs), the only thing that could be null is `SyncContextUtil.UnitySynchronizationContext`. This would happen if the `SyncContextUtil.Install` method isn't run: https://github.com/modesttree/Unity3dAsyncAwaitUtil/blob/master/UnityProject/Assets/Plugins/AsyncAwaitUtil/Source/Internal/SyncContextUtil.cs – Kevin Gosse Aug 06 '19 at 18:44
  • Unfortunately I know almost nothing about Unity, so I don't know how the `RuntimeInitializeOnLoadMethod` attribute works and in what conditions Unity is supposed to invoke the decorated method. That said, you can start by validating my hypothesis by checking whether `SyncContextUtil.UnitySynchronizationContext` is null before calling `await request.SendWebRequest()` – Kevin Gosse Aug 06 '19 at 18:45
  • Thanks Kevin, I was hoping it was something silly I was doing wrong. I'll dig a bit deeper and try to trace back the source of the exception. – G4bri3l Aug 06 '19 at 18:47
  • Yeah the SyncContextUtil.UnitySynchronizationContext is null and it's trying to invoke a .Post method on it but since that's null it throws that error. – G4bri3l Aug 06 '19 at 19:03
  • So either `SyncContextUtil.Install` isn't invoked by Unity, or `SynchronizationContext.Current` is null when it does. What is the value of `SyncContextUtil.UnityThreadId`? If it's equal to 0 then `SyncContextUtil.Install` isn't invoked. If it's equal to any other value then `SynchronizationContext.Current` is null. – Kevin Gosse Aug 06 '19 at 19:10
  • Found the culprit! So Unity calls the "RuntimeInitializeOnLoadMethod" only in Play mode which calls the "Install". Since I am using this in Editor mode that call is not triggered, they suggested a fix for it in one of the issues in the repo (https://github.com/modesttree/Unity3dAsyncAwaitUtil/issues/9). So I must have activate Play mode for a second which triggered the Install method, then stopped it but from then on it would work. – G4bri3l Aug 06 '19 at 19:10
  • I then completely closed Unity and reopened it, without ever using play mode the Install method was never triggered and nothing would work even though I did not change anything in my code. So that explains this odd behavior. – G4bri3l Aug 06 '19 at 19:11
  • Well played! Now you can summarize that in an answer for future generations :) – Kevin Gosse Aug 06 '19 at 19:13
  • Exactly, thanks for the help! – G4bri3l Aug 06 '19 at 19:16

1 Answers1

5

Found the culprit!

The code per se works just fine and there are no errors with it. The main issue (thanks to some digging by @KevinGosse) is triggered by the Async/Await plugin code.

Once I put a debugger at line 128 of IEnumeratorAwaitExtensions.cs I noticed that SyncContextUtil.UnitySynchronizationContext was null and that's what was causing the NullReference error.

The SyncContextUtil.UnitySynchronizationContext is initilized in the SyncContextUtil.cs script file thanks to the Install method. So the question was, why isn't this method triggered ?

Looking at the source you can see the decorator [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] which simply invokes the Install method when Play Mode is triggered! But I am working in Editor Mode so the Install method is not triggered and SyncContextUtil.UnitySynchronizationContext remains null no matter what I do.

Turns out this issue is know in the repo and you can check it out here. They propose a solution to this exact issue to allow the use of Async/Await in Editor Mode.

For me it was very puzzling why it worked at some point and then it wasn't without me making any code change. The reason was that I activated the Play Mode for a second, then stopped it and continued working in Editor Mode. As we mentioned before starting the Play Mode would call the Install method which is what makes everything work properly. I then closed Unity, restarted it and (without ever activating Play Mode) none of my scripts were working anymore!

I will now follow the approach suggested in the issue comment to make Async/Await work as expected in Editor Mode as well. I hope this answer will help others as well!

EDIT

I added a decorator to the Install method and I don't have the error anymore. Here is the modified code:

namespace UnityAsyncAwaitUtil
{
    public static class SyncContextUtil
    {
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        [InitializeOnLoadMethod]
        static void Install()
        {
            // As suggested by @derHugo you can add a null check here
            // which will prevent initializing these parameters twice
            // see his comment for more info
            if (UnitySynchronizationContext == null) {
              UnitySynchronizationContext = SynchronizationContext.Current;
            }
            if (UnityThreadId == null) {
              UnityThreadId = Thread.CurrentThread.ManagedThreadId;
            }
        }

        public static int UnityThreadId
        {
            get; private set;
        }

        public static SynchronizationContext UnitySynchronizationContext
        {
            get; private set;
        }
    }
}

I have tested this for use of Async/Await in Editor Mode so please run your own tests to make sure this change works for your case as well.

G4bri3l
  • 4,996
  • 4
  • 31
  • 53
  • 2
    You might want to remove the `RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)` or add a check. I'm not sure how these are handled when multiple attributes are used but I would guess it might get called twice on runtime (only in editor though) now since `InitializeOnLoadMethod` is also called when entering play mode – derHugo Aug 07 '19 at 04:23
  • Hi, thanks for this, it got my Editor code working. However it also creates a gameObject called "AsyncCoroutineRunner" for every time I call my code. Did this happen to you to and if so, how'd you manage this? – Gijs Beijer Apr 21 '20 at 11:07