2012年12月21日金曜日

コンソールアプリケーション3(Delphi Advent Calendar 2012-12-21)


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 uses
006  System.SysUtils, Winapi.Windows;
007  
008 function Exec(const iCommand, iParam: String): String;
009 var
010  ReadHandle, WriteHandle: THandle;
011  SA: TSecurityAttributes;
012  SI: TStartUpInfo;
013  PI: TProcessInformation;
014  Buffer: RawByteString;
015  Len: Cardinal;
016  
017  // パイプから値を読み出す
018  procedure ReadResult;
019  var
020  Count: DWORD;
021  ReadableByte: DWORD;
022  Data: RawByteString;
023  begin
024  // 読み出しバッファをクリア
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 begin
033  while
034  (ReadFile(ReadHandle, PRawByteString(Buffer)^, Len, Count, nil))
035  do begin
036  Data := Data + RawByteString(Copy(Buffer, 1, Count));
037  
038  if (Count >= ReadableByte) then
039  Break;
040  end;
041  
042  Result := Result + Data;
043  end;
044  end;
045  
046 begin
047  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  try
056  // StartInfo を初期化
057  ZeroMemory(@SI, SizeOf(SI));
058  with SI do begin
059  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  then
078  Exit;
079  
080  // 読み出しバッファを 4096[byte] 確保
081  SetLength(Buffer, 4096);
082  Len := Length(Buffer);
083  
084  with PI do begin
085  // プロセスが終了するまで、パイプを読み出す
086  while (WaitForSingleObject(hProcess, 100) = WAIT_TIMEOUT) do
087  ReadResult;
088  
089  ReadResult;
090  
091  // プロセスを閉じる
092  CloseHandle(hProcess);
093  CloseHandle(hThread);
094  end;
095  finally
096  // パイプを閉じる
097  CloseHandle(WriteHandle);
098  CloseHandle(ReadHandle);
099  end;
100 end;
101  
102 begin
103  // 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 件のコメント:

コメントを投稿