enumなんとかをfor..inで、その1
for..in文法は、使う側は便利ですが、作る側はコールバック関数のほうが便利です。
そんなわけでC#のyieldからIEnumeratorの類をネイティブでやろうって話です。CreateFiber一発で片付くとか言うの禁止。
これはいつか作った高階関数版、Ada2005風に言うならdownward closure版のEnumWindowsです。使い方はこちら参照。
type CEachWindow = function(Wnd: HWND; Context: Pointer): Boolean cdecl; procedure EnumerateWindows(Proc: CEachWindow; Context: Pointer); forward; ... function EnumWindowsProc(Wnd: HWND; var D: TMethod): LongBool; stdcall; asm mov EDX, D mov EAX, TMethod[EDX].Data push EAX mov EAX, Wnd push EAX mov ECX, TMethod[EDX].Code call ECX add esp, 8 xor al, 1 cbw cwde end; procedure EnumerateWindows(Proc: CEachWindow; Context: Pointer); var D: TMethod; begin D.Code := @Proc; D.Data := Context; EnumWindows(@EnumWindowsProc, LPARAM(@D)) end;
この手のEnum系コールバック関数を、書き換えることなく、for..inで無理やり使えるようにするにはどうすればいいか。
Fiberだと一発ですが、ループのためだけにわざわざ新しくスタックとコンテキスト作って、なんてのはちょっと富豪的に過ぎると思うので、一本のスタックを行ったり来たりすることにします。
for..in側から見れば、初回のMoveNextがesp動かしてスタック上になにか確保しただけの状態。
type TWindowEnumerator = class(TObject) caller_ebp, caller_eip: Pointer; enum_ebx, enum_esi, enum_edi, enum_eip: Pointer; FCurrent: HWND; function MoveNext: Boolean; function GetCurrent: HWND; property Current: HWND read GetCurrent; end; function Each(Wnd: HWND; Self: TWindowEnumerator): Boolean cdecl; asm mov edx, Wnd mov eax, Self push ebp mov TWindowEnumerator[eax].FCurrent, edx mov TWindowEnumerator[eax].enum_ebx, ebx mov TWindowEnumerator[eax].enum_esi, esi mov TWindowEnumerator[eax].enum_edi, edi mov TWindowEnumerator[eax].enum_eip, offset @Next mov edx, TWindowEnumerator[eax].caller_eip mov ebp, TWindowEnumerator[eax].caller_ebp mov eax, 1 jmp edx @Next: pop ebp mov eax, Self mov al, 0 end; function TWindowEnumerator.MoveNext: Boolean; asm mov edx, TWindowEnumerator[eax].caller_eip test edx, edx jnz @Resume mov TWindowEnumerator[eax].caller_ebp, ebp mov edx, [esp] mov TWindowEnumerator[eax].caller_eip, edx mov edx, eax mov eax, offset Each call EnumerateWindows mov eax, 0 ret @Resume: mov edx, TWindowEnumerator[eax].FCurrent test edx, edx jz @End mov ebx, TWindowEnumerator[eax].enum_ebx mov esi, TWindowEnumerator[eax].enum_esi mov edi, TWindowEnumerator[eax].enum_edi mov edx, TWindowEnumerator[eax].enum_eip add esp, 4 jmp edx @End: xor eax, eax end; function TWindowEnumerator.GetCurrent: HWND; begin Result := FCurrent; end; type TEnumWindows = record function GetEnumerator: TWindowEnumerator; end; function TEnumWindows.GetEnumerator: TWindowEnumerator; begin Result := TWindowEnumerator.Create end;
問題点は、例外どころかBreakで死ねること。
ダメじゃん。
なお初回のMoveNextではなく、CreateでEnumWindowsに突入した場合、呼び出しもとがその後に例外ハンドラを設定するので、それはそれで微妙なのではあります…が、デストラクタで一旦EnumWindowsに戻すことを考えたらそちらのほうがまだ道はあるか?
…どっちにしろデストラクタが呼ばれるのは例外ハンドラのコンテキストなので、スタックかき回されてしまってますからねえ…。