Tutorial 21: Pipe

このチュートリアルではパイプが何か、何のために使えるのか、などについて深く勉強する。 さらに興味深くするために、エディットコントロールの背景と文字の色をどのようにすれば変更できるのか、というテクニックも紹介する。
 ソース   リソース   実行結果 

Theory:

パイプは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

この関数が成功すれば、戻り値は非0の値となり、失敗すれば0となる。

呼び出しが成功すれば、2つのハンドルを取得する。1つは読み込み用で1つは書き込み用だ。 これらを使用して、子コンソールアプリの標準出力を自分の作成したプロセスへリダイレクトするために必要な手順を説明しよう。 ただし、この方法はBorland's Win32APIリファレンスとは違っている。 リファレンスでは親プロセスはコンソールアプリと仮定しているので、子アプリは親から標準ハンドルを譲り受けている。 しかし、たいていはコンソールアプリからGUIアプリへと出力をリダイレクトしたいものである。

  1. CreatePipe関数をCALLして匿名パイプを作成する。SECURITY_ATTRIBUTES構造体のbInheritableメンバをTRUEにセットするのを忘れてはいけない。そうすれば、ハンドルは継承可能になる。
  2. 子コンソールアプリをロードするために使用するので、CreateProcess関数への引数を準備しなければならない。重要なパラメータはSTARTUPINFO構造体である。この構造体は子プロセスが表示されるときに、子プロセスのメインウィンドウが表示されるかどうかを決定する。この構造体は非常に重要なもので、メインウィンドウを隠したり、子プロセスへパイプハンドルを渡したりできる。以下にセットしなければならないメンバを説明する。
    • cb
      STARTUPINFO構造体のサイズ
    • dwFlags
      構造体のどのメンバが有効であるかを決定するビットフラグで、これにより、メインウィンドウを表示したり隠したりといった状態を調整できる。今回は、STARTF_USESHOWWINDOW と STARTF_USESTDHANDLES を使用する。
    • hStdOutput and hStdError
      子プロセスが標準出力、標準エラー出力のハンドルとして使用して欲しいハンドルを指定する。今回の例では、標準出力ハンドル、標準エラー出力ハンドルとして、パイプへの書き込みハンドルを渡す。つまり、子プロセスの標準出力、標準エラー出力への何らかの出力はその引数のパイプを通して親プロセスへと渡されることになる。
    • wShowWindow
      メインウィンドウの表示、非表示状態を決定する。今回の例では、子アプリのウィンドウは表示したくないので、SW_HIDEを指定する。
  3. CreateProcess関数をCLALして子アプリをロードする。CreateProcess関数が成功しても、まだ子プロセスは休止状態だ。ただメモリにロードされただけで、実行はなされていない。
  4. パイプの書き込みハンドルを閉じるのを忘れてはいけない。なぜなら、親プロセスは書き込みハンドルを使用しないからだ。もし複数から書き込まれるようになればパイプは動作しないので、パイプからデータを読む前に絶対に閉じなければならない。しかし、CreateProcess関数をCALLする前に書き込みハンドルを閉じてはいけない。もしCALLしてしまうとパイプが破壊されてしまう。なので、閉じるタイミングはCreateProcess関数を読んだ後で、パイプからデータを読み込む前だ。
  5. ReadFile関数により読み込みパイプからデータを読み込める。ReadFile関数により子プロセスは起動モードとなる。そして、標準出力ハンドルへ何か書き込むのだが、そのデータは読み込みパイプを通ることになり、データが次々と送られてくる。データは全部がいっぺんに送られてくるわけではないので、戻り値が0になるまでReadFile関数を何回もCALLする。0になれば、全部のデータを読み終えたということになる。別にその読み込んだデータに対してどんな操作をしてもかまわないし、この例ではエディットコントロールに送信している。
  6. パイプの読み込みハンドルを閉じる。

Example:

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD

.const
IDR_MAINMENU equ 101        ; the ID of the main menu
IDM_ASSEMBLE equ 40001

.data
ClassName           db "PipeWinClass",0
AppName             db "One-way Pipe Example",0 EditClass db "EDIT",0
CreatePipeError    db "Error during pipe creation",0
CreateProcessError    db "Error during process creation",0
CommandLine    db "ml /c /coff /Cp test.asm",0

.data?
hInstance HINSTANCE ?
hwndEdit dd ?

.code
start:
   invoke GetModuleHandle, NULL
   mov hInstance,eax
   invoke WinMain, hInstance,NULL,NULL, SW_SHOWDEFAULT
   invoke ExitProcess,eax

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,CmdShow:DWORD
   LOCAL wc:WNDCLASSEX
   LOCAL msg:MSG
   LOCAL hwnd:HWND
   mov wc.cbSize,SIZEOF WNDCLASSEX
   mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc
   mov wc.cbClsExtra,NULL
   mov wc.cbWndExtra,NULL
   push hInst
   pop wc.hInstance
   mov wc.hbrBackground,COLOR_APPWORKSPACE
   mov wc.lpszMenuName,IDR_MAINMENU
   mov wc.lpszClassName,OFFSET ClassName
   invoke LoadIcon,NULL,IDI_APPLICATION
   mov wc.hIcon,eax
   mov wc.hIconSm,eax
   invoke LoadCursor,NULL,IDC_ARROW
   mov wc.hCursor,eax
   invoke RegisterClassEx, addr wc
   invoke CreateWindowEx,WS_EX_CLIENTEDGE,ADDR ClassName,ADDR AppName,\
          WS_OVERLAPPEDWINDOW+WS_VISIBLE,CW_USEDEFAULT,\
          CW_USEDEFAULT,400,200,NULL,NULL, hInst,NULL
   mov hwnd,eax
   .while TRUE
       invoke GetMessage, ADDR msg,NULL,0,0
       .BREAK .IF (!eax)
       invoke TranslateMessage, ADDR msg
       invoke DispatchMessage, ADDR msg
   .endw
   mov eax,msg.wParam
   ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
   LOCAL rect:RECT
   LOCAL hRead:DWORD
   LOCAL hWrite:DWORD
   LOCAL startupinfo:STARTUPINFO
   LOCAL pinfo:PROCESS_INFORMATION
   LOCAL buffer[1024]:byte
   LOCAL bytesRead:DWORD
   LOCAL hdc:DWORD
   LOCAL sat:SECURITY_ATTRIBUTES
   .if uMsg==WM_CREATE
       invoke CreateWindowEx,NULL,addr EditClass, NULL,\
              WS_CHILD+ WS_VISIBLE+ ES_MULTILINE+ ES_AUTOHSCROLL+ ES_AUTOVSCROLL,\
              0, 0, 0, 0, hWnd, NULL, hInstance, NULL
       mov hwndEdit,eax
   .elseif uMsg==WM_CTLCOLOREDIT
       invoke SetTextColor,wParam,Yellow
       invoke SetBkColor,wParam,Black
      invoke GetStockObject,BLACK_BRUSH
       ret
   .elseif uMsg==WM_SIZE
       mov edx,lParam
       mov ecx,edx
       shr ecx,16
       and edx,0ffffh
       invoke MoveWindow,hwndEdit,0,0,edx,ecx,TRUE
   .elseif uMsg==WM_COMMAND
      .if lParam==0
           mov eax,wParam
           .if ax==IDM_ASSEMBLE
               mov sat.nLength,sizeof SECURITY_ATTRIBUTES
               mov sat.lpSecurityDescriptor,NULL
               mov sat.bInheritHandle,TRUE
               invoke CreatePipe,addr hRead,addr hWrite,addr sat,NULL
               .if eax==NULL
                   invoke MessageBox, hWnd, addr CreatePipeError, addr AppName, MB_ICONERROR+ MB_OK
               .else
                   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
                   invoke CreateProcess, NULL, addr CommandLine, NULL, NULL, TRUE, NULL, NULL, NULL,\
                                         addr startupinfo, addr pinfo
                   .if eax==NULL
                       invoke MessageBox,hWnd,addr CreateProcessError,addr AppName,MB_ICONERROR+MB_OK
                   .else
                       invoke CloseHandle,hWrite
                       .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
                   .endif
                   invoke CloseHandle,hRead
               .endif
           .endif
       .endif
   .elseif uMsg==WM_DESTROY
       invoke PostQuitMessage,NULL
   .else
       invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret
   .endif
   xor eax,eax
   ret
WndProc endp
end start

Analysis:

上の例では、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
    ret

SetTextColor関数により文字の色を黄色に変え、背景の色を黒に変更している。 最後に、Windowsにより黒ブラシのハンドルが渡される。 そして、WM_CTLCOLOREDITメッセージで、Windowsがエディットコンロールの背景を描画するためのブラシへのハンドルを返さなければならない。 この例では、背景を黒にしたいので、Windowsに黒ブラシのハンドルを返している。

ユーザがAssembleメニューアイテムを選択したとき、匿名パイプが作成される。

.if ax==IDM_ASSEMBLE
    mov sat.nLength,sizeof SECURITY_ATTRIBUTES
    mov sat.lpSecurityDescriptor,NULL
    mov sat.bInheritHandle,TRUE

CreatePipe関数を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,hRead

ReadFile関数がNULLを返せば、ループを抜け、読み込みハンドルをクローズする。


[戻る]