UniRx
のソースを呼んでいるとAsyncMethodBuilder
の実装が以下のようになっていました。
// UniRx AsyncUniTaskMethodBuilder.cs より引用
// 5. AwaitOnCompleted
[DebuggerHidden]
public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
where TAwaiter : INotifyCompletion
where TStateMachine : IAsyncStateMachine
{
if (moveNext == null)
{
if (promise == null)
{
promise = new UniTaskCompletionSource(); // built future.
}
var runner = new MoveNextRunner<TStateMachine>();
moveNext = runner.Run;
runner.StateMachine = stateMachine; // set after create delegate.
}
awaiter.OnCompleted(moveNext);
}
set after create delegate
と書かれている部分でなぜそうでなければいけないか疑問だったのですが、
ようやく理解できたので備忘録としてメモしておきます。
メモリ初期状態。
# スタック領域
StateMachine
- AsyncMethodBuilder
- その他ローカル変数
# ヒープ領域
なし
Awaiter
に後続処理を依頼するためにはAction
を渡す必要がある。
Action
はクラスオブジェクトなのでヒープ領域に作成しなければならない。
よって最終的には以下のようなメモリレイアウトになる。
# スタック領域
StateMachine
- AsyncMethodBuilder
- その他ローカル変数
# ヒープ領域
Action
- StateMachine Pointer
- Method Address
StateMachine(スタック領域からのコピー, Actionから参照される)
- AsyncMethodBuilder
- その他ローカル変数
これを踏まえて再度UniRx
のコードを見る。
// UniRx AsyncUniTaskMethodBuilder.cs より引用
var runner = new MoveNextRunner<TStateMachine>();
moveNext = runner.Run;
runner.StateMachine = stateMachine; // set after create delegate.
もしrunner.StateMachine
への代入がdelegate
を作るより前に行われた場合、
-
StateMachine
をスタック領域からヒープ領域にコピー - スタック領域の
AsyncMethodBuilder
のmoveNext
を書き換える
の順番で処理が行われる。
# スタック領域
StateMachine
- AsyncMethodBuilder <- ここのmoveNextが書き換わる
- その他ローカル変数
# ヒープ領域
StateMachine
- AsyncMethodBuilder <- こっちは書き換える前にコピーしてしまったのでそのまま
- その他ローカル変数
ヒープ領域にコピーした側のmoveNext
を書き換えなければ明らかにまずい。
なのでスタック領域のmoveNext
を先に書き換えてその後ヒープ領域にコピーする必要がある。
StateMachine
のMoveNext
を直接AsyncMethodBuilder
のmoveNext
に入れることはできない。
delegate
を作った時点でAsyncMethodBuilder
がヒープ領域にコピーされてしまうからである。
必要条件としてdelegate
を作った後にヒープ領域にコピーできる必要がある。
UniRx
ではそれを実現するためにMoveNextRunner
というラッパーを挟んでいる。
なおDebug
ビルドの場合はStateMachine
がclass
として作られるので順番が違っても動いてしまう。