Delphi Advent Calendar 2012 12/21 の記事です。
前回、コンソールアプリケーションで Read/Write について述べました。
今回は、Read/Write の標準入出力先を変更してみます。
Read/Write にはファイル変数を指定することで、ファイルに値を出力したり、値をファイルから読み出したりできます。
しかし、それはあくまで入出力先を変数として与えただけで、標準入出力先が変わった訳ではありません。
それでは、標準入出力先を変更するにはどうすれば良いのでしょうか?
StartUpInfo に、その鍵があります。
StartUpInfo とは、CreateProcess でプロセスを生成するときに渡すパラメータの1つです。
StartUpInfo は構造体ですが、ここに次の重要なパラメータがあります。
hStdInput | 標準入力のハンドル |
hStdOutput | 標準出力のハンドル |
hStdError | 標準エラー出力のハンドル |
このパラメータの説明にあるとおり、ここにハンドルを指定することで、標準入出力先を変更できるのです!
ちなみに、UNIX では、標準入力のハンドルは 0, 標準出力のハンドルは 1 と、決まった値になっています。
Windows の場合は、ハンドルは決まっていません。
その代わり GetStdHandle という API を使って標準入出力のハンドルを取得できます。
それはそうと、実際に標準入出力先を変更してみます。
ハンドルに指定できるのは CreateFile などで返されるハンドルです。
つまり、ファイルハンドルを指定すれば、ファイルに出力されます。
今回はパイプを使おうと思います。
パイプを作るには CreatePipe という関数を使います。
パイプは WriteHandle に対して書き込まれた値を ReadHandle で読み出すことができる通信路です。
とりあえず、今回のソースを全文記載します。
001 program Project1;002 003 {$APPTYPE CONSOLE}004 005 uses006  System.SysUtils, Winapi.Windows;007 008 function Exec(const iCommand, iParam: String): String;009 var010  ReadHandle, WriteHandle: THandle;011  SA: TSecurityAttributes;012  SI: TStartUpInfo;013  PI: TProcessInformation;014  Buffer: RawByteString;015  Len: Cardinal;016 017  // パイプから値を読み出す018  procedure ReadResult;019  var020  Count: DWORD;021  ReadableByte: DWORD;022  Data: RawByteString;023  begin024  // 読み出しバッファをクリア025  ZeroMemory(PRawByteString(Buffer), Len);026 027  // パイプに読み出せるバイト数がいくつあるのか調べる028  PeekNamedPipe(ReadHandle, PRawByteString(Buffer), Len, nil, nil, nil);029  ReadableByte := Length(Trim(String(Buffer)));030 031  // 読み込める文字列があるなら032  if (ReadableByte > 0) then begin033  while034  (ReadFile(ReadHandle, PRawByteString(Buffer)^, Len, Count, nil))035  do begin036  Data := Data + RawByteString(Copy(Buffer, 1, Count));037 038  if (Count >= ReadableByte) then039  Break;040  end;041 042  Result := Result + Data;043  end;044  end;045 046 begin047  Result := '';048 049  ZeroMemory(@SA, SizeOf(SA));050  SA.nLength := SizeOf(SA);051  SA.bInheritHandle := True;052 053  // パイプを作る054  CreatePipe(ReadHandle, WriteHandle, @SA, 0);055  try056  // StartInfo を初期化057  ZeroMemory(@SI, SizeOf(SI));058  with SI do begin059  cb := SizeOf(SI);060  dwFlags := STARTF_USESTDHANDLES; // 標準入出力ハンドルを使います!宣言061  hStdOutput := WriteHandle; // 標準出力を出力パイプに変更062  hStdError := WriteHandle; // 標準エラー出力を出力パイプに変更063  end;064 065  // プロセスを作成066  if (not CreateProcess(067  PChar(iCommand),068  PChar(iParam),069  nil,070  nil,071  True,072  0,073  nil,074  nil,075  SI,076  PI))077  then078  Exit;079 080  // 読み出しバッファを 4096[byte] 確保081  SetLength(Buffer, 4096);082  Len := Length(Buffer);083 084  with PI do begin085  // プロセスが終了するまで、パイプを読み出す086  while (WaitForSingleObject(hProcess, 100) = WAIT_TIMEOUT) do087  ReadResult;088 089  ReadResult;090 091  // プロセスを閉じる092  CloseHandle(hProcess);093  CloseHandle(hThread);094  end;095  finally096  // パイプを閉じる097  CloseHandle(WriteHandle);098  CloseHandle(ReadHandle);099  end;100 end;101 102 begin103  // dir の結果を出力104  Writeln(Exec('C:\Windows\System32\CMD.exe', '/C dir'));105  Readln;106 end.
このソースコードでは、コマンドラインで dir を呼んだ結果を表示します。
結果は、こんな風になります。
重要なのは 60~62 行の StartUpInfo の初期化部です。
前に記載したとおり hStdOutput, hStdError にパイプの書き込みハンドルを入れています。
この hStdOutput と hStdError を有効にするためにフラグに STARTF_USESTDHANDLES を代入しています。
この値を設定しないと標準入出力ハンドルは使用されません。
そして、起動されたコンソールアプリケーションは、設定された書き込み用パイプハンドル(WriteHandle)に値を書き込みます。
値は読み込み用パイプハンドル(ReadHandle)からから読み出すことができます(34行目)。
このようにちょっと手間ですが、標準入出力の値を変更することができました。
次回は、これらの API を使ってコンソールをデバッグ用出力として使う方法を紹介したいと思います。
0 件のコメント:
コメントを投稿