UniTask icon indicating copy to clipboard operation
UniTask copied to clipboard

Awaiting and then reading result causes an exception

Open rollie42 opened this issue 2 years ago • 2 comments

This code throws in GetResult:

protected async virtual UniTask Init() {
        foreach (var prop in GetProperties()) {
            if (prop.GetValue(this) == null) {
                var methodInfo = prop.PropertyType.GetMethod("Create", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy);
                var task = methodInfo.Invoke(null, new object[]{}); // returns UniTask<something>
                var baseTask = task.GetType().GetMethod("AsUniTask").Invoke(task, new object[]{}); // returns UniTask
                await (UniTask)baseTask;

                var awaiter = task.GetType().GetMethod("GetAwaiter").Invoke(task, new object[]{}); // returns UniTask<something>.Awaiter
                
                // After I updated some versions, this now throws; but if you look at the above, the value from the Create() method has
                // never actually been returned by any method invocation.
                var result = awaiter.GetType().GetMethod("GetResult").Invoke(awaiter, new object[]{}); // returns 'something'
                prop.SetValue(this, result);
            }
        }
    }

Is this expected? I would think the resulting value sticks around until all refs to the UniTask itself are removed. I could try using Preserve() to 'await multiple', but I'm not actually awaiting more than once.

rollie42 avatar May 08 '22 07:05 rollie42

Yes, that is expected, as once a UniTask instance is awaited, it is no longer valid, unless it's preserved. Since you're using reflection to handle async invokes here, I suggest refactoring your code to something more like this:

public class MyType
{
    protected async virtual UniTask Init()
    {
        var openGenericSetPropertyMethod = typeof(MyType)
            .GetMethods(BindingFlags.Instance | BindingFlags.NonPublic)
            .First(m => m.Name == nameof(SetPropertyAsync));

        foreach (var prop in GetProperties())
        {
            if (prop.GetValue(this) == null)
            {
                var methodInfo = prop.PropertyType.GetMethod("Create", BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy); // returns UniTask<T>
                var resultType = methodInfo.ReturnType.GetGenericArguments()[0]; // typeof(T)
                var asyncMethod = openGenericSetPropertyMethod.MakeGenericMethod(new Type[] { resultType });

                await (UniTask) asyncMethod.Invoke(this, new object[] { prop, methodInfo });
            }
        }
    }

    private async UniTask SetPropertyAsync<T>(PropertyInfo propertyInfo, MethodInfo methodInfo)
    {
        T result = await (UniTask<T>) methodInfo.Invoke(null, Array.Empty<object>());
        propertyInfo.SetValue(this, result);
    }
}

[Edit] This code might not work in IL2CPP prior to Unity 2022.1 if the T is a struct or primitive. I think for that you will have to use the Preserve() method.

timcassell avatar Aug 03 '22 04:08 timcassell

@timcassell Thanks for that! That's an interesting way to get around the issue - I was hoping there was some way to get a return value from an await by just telling the runtime the specific type it should expect, but I couldn't find any support for doing that. I ended up just using Preserve, but never like running steps that conceptually shouldn't be required. T in this case should always be a class, if I recall correctly, though I am using 2021 LTS.

rollie42 avatar Sep 06 '22 04:09 rollie42

This issue is stale because it has been open 90 days with no activity. Remove stale label or comment or this will be closed in 7 days.

github-actions[bot] avatar Dec 06 '22 00:12 github-actions[bot]