このチュートリアルではパイプが何か、何のために使えるのか、などについて深く勉強する。 さらに興味深くするために、エディットコントロールの背景と文字の色をどのようにすれば変更できるのか、というテクニックも紹介する。
ソース | リソース | 実行結果 |
|
パイプは2つの終端をつなぐものである。 ここでいう「2つの終端」とは、2つのプロセスの入出力のことであり、2つのプロセスの間でデータを交換することができる。 2つのプロセスは違うプロセスでも同じプロセスであってもよい。 これはまるでトランシーバのようで、一つを自分、もう一つを別に人に持たせてコミュニケーションがとれるようになる。
パイプには2つのタイプがある。匿名パイプと名前付きパイプだ。 匿名パイプは、えー、匿名である。つまり、その名前を知らなくても使用できる。 名前付きパイプはその逆で、使用するには名前を知らなくてはならない。
また、パイプの属性、「単方向」と「双方向」とによる分類もできる。 単方向パイプでは、どちらからどちらにデータが流れるか決まっているが、 双方向パイプではどちらからでもデータの交換が可能である。
匿名パイプは常に単方向であるが、名前付きパイプは単方向、双方向、どちらにもなれる。 名前付きパイプはネットワーク環境でよく使われ、主にクラサバシステム用だ。
このチュートリアルでは、匿名パイプについて詳細に説明する。 匿名パイプの主な目的は、親プロセスと子プロセス、もしくは子プロセス同士のコミュニケーションである。
匿名パイプは、コンソールアプリでは本当に便利である。 コンソールアプリとはコンソールから入力、出力を行うWin32プログラムの一種で、 DOS窓のようなものだが、コンソールアプリは完全に32ビットプログラムである。 GUI関数も使用でき、GUIプログラムと同じなのだが、たまたまコンソールを使用しているだけである。
コンソールアプリは入出力に3つのハンドルを使用する。 3つとは、「標準入力」「標準出力」「標準エラー出力」の3つである。 標準入力ハンドルはコンソールから情報を取得するのに、 標準出力ハンドルはコンソールに情報を出力するのに使用する。 標準エラー出力ハンドルはリダイレクト(出力先をファイルなどに変更すること)できない(ほんとはやろうと思えばできるが)のでエラーを報告するために使用する。
コンソールアプリはGetStdHandle関数をCALLすることにより、これら3つの標準ハンドルのどれでも欲しいハンドルを取得できる。 GUIアプリはコンソールを使用しないので、もしGetStdHandle関数をCALLしてもエラーする。 もし本当にコンソールが使用したければ、AllocConsole関数により新しいコンソールを作成することができるのだが、 FreeConsole関数をCALLしてコンソールを解放することを忘れてはならない。
匿名パイプは、子コンソールアプリの入出力をリダイレクトするためにもっともよく使用される。 そのとき、親プロセスはコンソールアプリでもGUIアプリでもいいのだが、子アプリはコンソールアプリでなければならない。 既に説明したが、コンソールアプリは入出力に標準ハンドルを使用している。 もしコンソールアプリの入出力をリダイレクトしたければ、それら標準ハンドルをパイプのハンドルに変更すればよい。 コンソールアプリにとってはそのハンドルが変更されたことはわからずに、 標準ハンドルであるものとして使用し続ける。 これは一種のポリモーフィズム(多態)である。 ポリモーフィズムというのはOOP(オブジェクト指向)の専門用語である。 この方法は、子プロセスには何の変更も必要ないので非常に強力だ。
もう一つの問題は、コンソールアプリがどこから標準ハンドルを取得するかだ。 コンソールアプリが作成されたとき、親プロセスは2つの選択肢がある。 1つは、その子アプリ用に新たにコンソールを作成する方法と、 もう1つは、自分のコンソールを子アプリに譲る方法だ。 2つ目のアプローチは、親プロセスはコンソールアプリでなければならない。 もしGUIアプリなら、AllocConsole関数をまずCALLしてコンソールを作成しなければならない。
ではやってみよう。匿名パイプを作成するために、CreatePipe関数をCALLしなければならない。
CreatePipe proto pReadHandle : DWORD, \ pWriteHandle : DWORD, \ pPipeAttributes : DWORD, \ nBufferSize : DWORD
- pReadHandle
この変数が指しているアドレスにパイプを読み込むためのハンドルが返ってくる- pWriteHandle
この変数が指しているアドレスにパイプへ書き込むためのハンドルが返ってくる- pPipeAttributes
返ってくる読み書きハンドルを子プロセスに継承するかどうかを決めるために、SECURITY_ATTRIBUTES構造体へのポインタを指定する- nBufferSize
パイプが使用するバッファサイズを指定する。ただし、これは単なる参考値であり、実際にはその値にならないかもしれない。NULLを指定すると、デフォルトサイズが使用される。この関数が成功すれば、戻り値は非0の値となり、失敗すれば0となる。
呼び出しが成功すれば、2つのハンドルを取得する。1つは読み込み用で1つは書き込み用だ。 これらを使用して、子コンソールアプリの標準出力を自分の作成したプロセスへリダイレクトするために必要な手順を説明しよう。 ただし、この方法はBorland's Win32APIリファレンスとは違っている。 リファレンスでは親プロセスはコンソールアプリと仮定しているので、子アプリは親から標準ハンドルを譲り受けている。 しかし、たいていはコンソールアプリからGUIアプリへと出力をリダイレクトしたいものである。
- CreatePipe関数をCALLして匿名パイプを作成する。SECURITY_ATTRIBUTES構造体のbInheritableメンバをTRUEにセットするのを忘れてはいけない。そうすれば、ハンドルは継承可能になる。
- 子コンソールアプリをロードするために使用するので、CreateProcess関数への引数を準備しなければならない。重要なパラメータはSTARTUPINFO構造体である。この構造体は子プロセスが表示されるときに、子プロセスのメインウィンドウが表示されるかどうかを決定する。この構造体は非常に重要なもので、メインウィンドウを隠したり、子プロセスへパイプハンドルを渡したりできる。以下にセットしなければならないメンバを説明する。
- cb
STARTUPINFO構造体のサイズ- dwFlags
構造体のどのメンバが有効であるかを決定するビットフラグで、これにより、メインウィンドウを表示したり隠したりといった状態を調整できる。今回は、STARTF_USESHOWWINDOW と STARTF_USESTDHANDLES を使用する。- hStdOutput and hStdError
子プロセスが標準出力、標準エラー出力のハンドルとして使用して欲しいハンドルを指定する。今回の例では、標準出力ハンドル、標準エラー出力ハンドルとして、パイプへの書き込みハンドルを渡す。つまり、子プロセスの標準出力、標準エラー出力への何らかの出力はその引数のパイプを通して親プロセスへと渡されることになる。- wShowWindow
メインウィンドウの表示、非表示状態を決定する。今回の例では、子アプリのウィンドウは表示したくないので、SW_HIDEを指定する。- CreateProcess関数をCLALして子アプリをロードする。CreateProcess関数が成功しても、まだ子プロセスは休止状態だ。ただメモリにロードされただけで、実行はなされていない。
- パイプの書き込みハンドルを閉じるのを忘れてはいけない。なぜなら、親プロセスは書き込みハンドルを使用しないからだ。もし複数から書き込まれるようになればパイプは動作しないので、パイプからデータを読む前に絶対に閉じなければならない。しかし、CreateProcess関数をCALLする前に書き込みハンドルを閉じてはいけない。もしCALLしてしまうとパイプが破壊されてしまう。なので、閉じるタイミングはCreateProcess関数を読んだ後で、パイプからデータを読み込む前だ。
- ReadFile関数により読み込みパイプからデータを読み込める。ReadFile関数により子プロセスは起動モードとなる。そして、標準出力ハンドルへ何か書き込むのだが、そのデータは読み込みパイプを通ることになり、データが次々と送られてくる。データは全部がいっぺんに送られてくるわけではないので、戻り値が0になるまでReadFile関数を何回もCALLする。0になれば、全部のデータを読み終えたということになる。別にその読み込んだデータに対してどんな操作をしてもかまわないし、この例ではエディットコントロールに送信している。
- パイプの読み込みハンドルを閉じる。
|
|
|
上の例では、ml.exeにtest.asmというファイル名をアセンブルしてもらい、 その出力結果をクライアントエリアのエディットコントロールへとリダイレクトしている。 プログラムがロードされたとき、いつものようにウィンドウクラスを登録し、メインウィンドウを作成する。 メインウィンドウを作成する時にまずはじめにやることは、ml.exeの出力結果を表示するためのエディットコントロールを作成することだ。
おもしろいところは、エディットコントロールの文字と背景の色を変更するところだ。 エディットコントロールがクライアントエリアに表示されるときに、親ウィンドウにWM_CTLCOLOREDITメッセージを送信している。
その際、wParamは、エディットコントロールが表示されるクライアントエリアのデバコンのハンドルとなっており、 この機会に、HDCの特性を変更する。
.elseif uMsg==WM_CTLCOLOREDIT invoke SetTextColor,wParam,Yellow invoke SetTextColor,wParam,Black invoke GetStockObject,BLACK_BRUSH retSetTextColor関数により文字の色を黄色に変え、背景の色を黒に変更している。 最後に、Windowsにより黒ブラシのハンドルが渡される。 そして、WM_CTLCOLOREDITメッセージで、Windowsがエディットコンロールの背景を描画するためのブラシへのハンドルを返さなければならない。 この例では、背景を黒にしたいので、Windowsに黒ブラシのハンドルを返している。
ユーザがAssembleメニューアイテムを選択したとき、匿名パイプが作成される。
.if ax==IDM_ASSEMBLE mov sat.nLength,sizeof SECURITY_ATTRIBUTES mov sat.lpSecurityDescriptor,NULL mov sat.bInheritHandle,TRUECreatePipe関数をCALLする前に、まずSECURITY_ATTRIBUTES構造体を設定しないといけない。 セキュリティについて何も気にしないのなら、lpSecurityDescriptorメンバはNULLでもよい。 そして、パイプハンドルを子プロセスへ継承させるために、bInheritHandleをTRUEにセットする。
invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULLその後、CreatePipe関数をCALLし、成功すれば、hReadとhWriteがそれぞれ、パイプへの読み込みハンドルと書き込みハンドルにセットされる。
mov startupinfo.cb,sizeof STARTUPINFO invoke GetStartupInfo,addr startupinfo mov eax, hWrite mov startupinfo.hStdOutput,eax mov startupinfo.hStdError,eax mov startupinfo.dwFlags, STARTF_USESHOWWINDOW+ STARTF_USESTDHANDLES mov startupinfo.wShowWindow,SW_HIDE次に、STARTUPINFO構造体をセットするのだが、まずGetStartupInfo関数をCALLして親プロセスのデフォルト値をセットする。 もしWin9xでもNTでも動作させるつもりなら、STARTUPINFO構造体をカスタマイズしなければならない。 GetStartupInfo関数をCALLした後、パイプへの書き込み、読み込みハンドルをhStdOutputとhStdErrorにコピーし、 子プロセスがデフォルトで使用する標準出力、標準エラー出力へのハンドルの代わりにそれらのハンドルを使うようにする。 それとあと、子プロセスのコンソールウィンドウも出したくないので、wShowWidowメンバにSW_HIDEを指定する。 最後に、hStdOutputとhStdError、wShowWindowが有効になるようにdwFlagsフラグに、STARTF_USESHOWWINDOW と STARTF_USESTDHANDLESを指定する。
invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL,\ addr startupinfo, addr pinfoそこで、CreateProcess関数をCALLし子プロセスを作成する。パイプハンドルを有効にするためにbInheritHandlesパラメータはTRUEにしなければならない。
invoke CloseHandle,hWrite子プロセスの作成が成功すれば、書き込みパイプをクローズしなければならない。 STARTUPINFO構造体を通して、子プロセスへ書き込みハンドルを渡したことを思い出そう。 もし書き込みハンドルを閉じなければ、2つの書き込む場所があることになり、パイプは動作しない。 CreateProcess関数をCALLした後で、かつパイプからデータを読み込む前に書き込みハンドルをクローズしなければならない。
.while TRUE invoke RtlZeroMemory,addr buffer,1024 invoke ReadFile,hRead,addr buffer,1023,addr bytesRead,NULL .if eax==NULL .break .endif invoke SendMessage,hwndEdit,EM_SETSEL,-1,0 invoke SendMessage,hwndEdit,EM_REPLACESEL,FALSE,addr buffer .endwこれで子プロセスの標準出力からデータを読み込む準備ができた。 パイプから全部のデータを読み込むまで無限にループしている。 RtlZeroMemory関数をCALLしてバッファを0にセットし、ReadFile関数をCALLする。 その際、ファイルハンドルではなくパイプのハンドルを引数にセットする。 この例では、最大1023バイトの文字しか読めないことに注意せよ。 なぜなら、エディットコントロールへ送信するデータはヌル終端文字列でなければならないからだ。
ReadFile関数がバッファにデータをセットして返ってきたら、そのデータをエディットコントロールに送信する。 しかしここでちょっとした問題が発生する。エディットコントロールにデータを出力するためにSetWindowText関数をCALLすれば、 既存のデータを上書きしてしまう。 ここでやりたいのは、既存のデータに「追加」していくことだ。
そのため、エディットコントロールにwParamを -1 にセットしてEM_SETSELメッセージを送信して文字の入力位置を一番最後に指定しておき、 次にEM_REPLACESELメッセージにより指定された場所からデータを追加していく。
invoke CloseHandle,hReadReadFile関数がNULLを返せば、ループを抜け、読み込みハンドルをクローズする。