-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Description
Description
In a console application in Visual Studio 2019, built to .Net Framework 4.7.2 and .Net Core 3.1, given this main method and async method builder, I am getting unexpected results.
AwaitOnCompleted, Task: NULL
AwaitOnCompleted, Task: Type: Promise, State: Pending
Task, Task: Type: Promise, State: Pending
SetResult, Task: NULL
It seems to be setting the task property properly, but then later it's not set. It seems very similar to the IL2CPP bug that Unity has with setting fields on a generic struct, however the method builder in question here doesn't fit that bill.
Interestingly, if I compile and run that exact same code in Unity (except for changing Console.WriteLine to UnityEngine.Debug.Log), it works properly in editor (.Net 4.x scripting runtime), although I have to change it to work with an IL2CPP build (which I didn't expect I would need to do in a VS Console application).
Also, if I change the PromiseMethodBuilder to a class instead of a struct, it works.
AwaitOnCompleted, Task: NULL
AwaitOnCompleted, Task: Type: Promise, State: Pending
Task, Task: Type: Promise, State: Pending
SetResult, Task: Type: Promise, State: Pending
Code
using Proto.Promises;
using System;
namespace PromiseTest
{
public class Program
{
public static void Main(string[] args)
{
AsyncPromise();
Console.ReadKey();
}
static void AsyncPromise()
{
// Create a promise to await so that the async function won't complete synchronously.
Promise.Deferred deferred = Promise.NewDeferred();
Promise promise = deferred.Promise;
_ = Async();
async Promise Async()
{
await promise;
}
deferred.Resolve();
Promise.Manager.HandleCompletesAndProgress();
}
}
}
namespace Proto.Promises.Async.CompilerServices
{
/// <summary>
/// This type and its members are intended for use by the compiler.
/// </summary>
[DebuggerNonUserCode]
public struct PromiseMethodBuilder
{
private Promise task;
// Using a promise object as its own continuer saves 16 bytes of object overhead (x64).
[DebuggerNonUserCode]
private abstract class AsyncPromise : Promise
{
// Cache the delegate to prevent new allocations.
public readonly Action continuation;
public AsyncPromise()
{
continuation = ContinueMethod;
}
protected abstract void ContinueMethod();
public void SetResult()
{
ResolveDirect();
}
public void SetException(Exception exception)
{
if (exception is OperationCanceledException)
{
CancelDirect(ref exception);
}
else
{
RejectDirect(ref exception, int.MinValue);
}
}
}
[DebuggerNonUserCode]
private sealed class AsyncPromise<TStateMachine> : AsyncPromise, Internal.ITreeHandleable where TStateMachine : IAsyncStateMachine
{
private static ValueLinkedStack<Internal.ITreeHandleable> _pool;
static AsyncPromise()
{
Internal.OnClearPool += () => _pool.Clear();
}
private TStateMachine _stateMachine;
public static AsyncPromise<TStateMachine> GetOrCreate(ref TStateMachine stateMachine)
{
var promise = _pool.IsNotEmpty ? (AsyncPromise<TStateMachine>) _pool.Pop() : new AsyncPromise<TStateMachine>();
promise.Reset();
promise._stateMachine = stateMachine;
return promise;
}
void Internal.ITreeHandleable.Handle()
{
ReleaseInternal();
}
protected override void Dispose()
{
base.Dispose();
_stateMachine = default;
if (Config.ObjectPooling != PoolType.None)
{
_pool.Push(this);
}
}
protected override void ContinueMethod()
{
_stateMachine.MoveNext();
}
}
public Promise Task
{
get
{
Console.WriteLine("Task, Task: " + (task?.ToString() ?? "NULL"));
return task;
}
private set => task = value;
}
public static PromiseMethodBuilder Create()
{
return new PromiseMethodBuilder();
}
public void SetException(Exception exception)
{
if (task is null)
{
if (exception is OperationCanceledException e)
{
task = Promise.Canceled(e);
}
else
{
task = Promise.Rejected(exception);
}
}
else
{
((AsyncPromise) task).SetException(exception);
}
}
public void SetResult()
{
Console.WriteLine("SetResult, Task: " + (task?.ToString() ?? "NULL"));
if (task is null)
{
task = Promise.Resolved();
}
else
{
((AsyncPromise) task).SetResult();
}
}
public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine
{
Console.WriteLine("AwaitOnCompleted, Task: " + (task?.ToString() ?? "NULL"));
SetContinuation(ref stateMachine);
Console.WriteLine("AwaitOnCompleted, Task: " + (task?.ToString() ?? "NULL"));
awaiter.OnCompleted(((AsyncPromise) task).continuation);
}
[SecuritySafeCritical]
public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : ICriticalNotifyCompletion
where TStateMachine : IAsyncStateMachine
{
SetContinuation(ref stateMachine);
awaiter.UnsafeOnCompleted(((AsyncPromise) task).continuation);
}
public void Start<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine
{
stateMachine.MoveNext();
}
public void SetStateMachine(IAsyncStateMachine stateMachine) { }
private void SetContinuation<TStateMachine>(ref TStateMachine stateMachine)
where TStateMachine : IAsyncStateMachine
{
if (task is null)
{
task = AsyncPromise<TStateMachine>.GetOrCreate(ref stateMachine);
}
}
}
}
Configuration
Visual Studio 2019 16.7.5
.Net Framework 4.7.2 and .Net Core 3.1
Windows 7 SP1, x64
AMD Phenom II x6 @ 2.8 GHz
Regression?
Not sure
Other information
The only workarounds are to either make the method builder a class instead of struct, or set all fields/properties in the Create method, and don't mutate the struct at all in the other methods.