DOBON.NET DOBON.NETプログラミング掲示板過去ログ

DLL内のprintf()による出力を得たい

環境/言語:[C#、.NET2.0]
分類:[.NET]

皆様、こんにちは。

【前提】
C#で作成したウィンドウアプリケーションから外部のDLL(アンマネージ)で定義されている関数をSystem.Runtime.InteropServicesのDLLImportを使用して呼び出しています。

【やりたいこと】
この関数内ではprintf()によって標準出力へメッセージが出力されています。この出力内容をテキストボックスに表示させたい。

実行時にDLLが読み込まれた後に何かする必要があるに違いないと思い、探していますがなかなか解法が見つかりません。外部プロセスの標準出力を得る方法は見つかったのですが…。

ご存知の方、いらっしゃいますでしょうか。
適当な TextWriter を作ってそれを Console.SetOut すればいいんじゃないですかね。
TextWriter は StringWriter 使うなり、TextBox に直接出力されるように自前で実装するなり。

【VB.NET】標準出力先を変更する
http://blog.livedoor.jp/akf0/archives/51333104.html

あとは、TextBoxに出力されるように修正する。
貴重な時間をありがとうございます。お二方にコメントいただいたので
すが、返信はこちらにつけます。

■No23211に返信(やじゅさんの記事)
> 【VB.NET】標準出力先を変更する
> http://blog.livedoor.jp/akf0/archives/51333104.html

リンク先の内容を参考に(そのままですが)やってみました。

private void button1_Click(object sender, EventArgs e)
{
    StringWriter sw = new StringWriter();
    Console.SetOut(sw);
    Console.WriteLine("hogehoge");
    txtOutput.Text = sw.ToString();
}

ボタンを押せば"hogehoge"が表示されます。Formのコンストラクタで
StringWriterのインスタンスを生成して、後から適当なボタンクリック
でConsole.WriteLineしてもStringWriterの内容をテキストボックスに表示
することができます。

DLLの関数を実行したあとにStringWriterの中身を見ても何も入ってきま
せん。OutputDebugString()が使われているかも、と疑いましたがそんなこ
とはありませんでした。

DLLの側から見えている標準出力と、呼び出し側のC# Windows アプリケー
ションから見えている標準出力は違うのでしょうか。
Console.SetOut は単純に Console.Write/WriteLine で書き込む先を変更するだけで、コンソールの奥のほうの実装してないみたいでした。
未確認で返答してしまってすいません。

さてそうなると、Console クラスではどうにもならないので Win32API に手を出すことになりそうです。
一般的には CreatePipe でパイプを作り、SetStdOut で標準出力に書き込みパイプを設定。
読み込みパイプの方はハンドルから FileStream を作成して、StreamReader を使って読み取り。
という形でしょうか。
ただ、printf にせよ cout にせよ、バッファリングによっては呼び出し直後にコンソールに出力されるとは限りません。
そっか、Console.SetOut では駄目なんですね。

プロセスにしてもいいなら、DLLを使うのを別EXEと作成し
本実行ファイルから、そのEXEを外部プロセスとして呼出して
標準出力を得るって感じになるでしょうか

[C# .NET] 外部プログラムの起動
http://ykun.exblog.jp/5128754/
■No23217に返信(Hongliangさんの記事)
> Console.SetOut は単純に Console.Write/WriteLine で書き込む先を変更するだけで、コンソールの奥のほうの実装してないみたいでした。

window アプリケーションがコンソールを使う、ましてや標準出力を使うこと自体盛り込まれていない。開発者に言わせればせっかくのGUIなんだから今更使わないでしょう、ということなのかもしれませんね。

> さてそうなると、Console クラスではどうにもならないので Win32API に手を出すことになりそうです。
> 一般的には CreatePipe でパイプを作り、SetStdOut で標準出力に書き込みパイプを設定。
> 読み込みパイプの方はハンドルから FileStream を作成して、StreamReader を使って読み取り。
> という形でしょうか。

ハードルが高そうなので少し時間がかかりそうですが試してみます。
私の頭の中ではこのように理解しました。

StreamReader←FileStream←(CreatePipe)→SetStdOut→標準出力

1.新しく作成したパイプをSetStdOutで標準出力の出力先にする。
2.そのパイプから出力を読み出すために、ハンドルを介してFileStreamを取得。
3.StreamReaderを介してFileStreamを読む。
やってみました。w32はDLLImport属性にてWin32APIを定義するのに使用して
いるクラスです。パイプで使用しているハンドルはIntPtrです。

lib.PrintStandard()はテスト用に作成したDLLに定義した関数で、引数に指定
された文字列を標準出力へ出力します(printf()してます)。

あるボタンのクリックイベントに以下のソースを記述しました。
----

w32.SECURITY_ATTRIBUTES security_attributes1 =
    new w32.SECURITY_ATTRIBUTES();
security_attributes1.nLength = 12;
security_attributes1.bInheritHandle = true;
IntPtr handle1 = IntPtr.Zero;

try
{
    if (!w32.CreatePipe(out hStdOutReadPipe, out hStdOutWritePipe,
                        ref security_attributes1, 0))
    {
        errNo = Marshal.GetLastWin32Error();
        throw new Win32Exception(errNo);
    }

    handle1 = w32.GetStdHandle(w32.STD_OUTPUT_HANDLE);
    w32.SetStdHandle(
        w32.STD_OUTPUT_HANDLE, hStdOutWritePipe);
    handle1 = w32.GetStdHandle(w32.STD_OUTPUT_HANDLE);

    if (!w32.DuplicateHandle(w32.GetCurrentProcess(), hStdOutReadPipe,
                             w32.GetCurrentProcess(), out hDupStdOutReadPipe,
                             0, false, w32.DUPLICATE_SAME_ACCESS))
    {
        errNo = Marshal.GetLastWin32Error();
        throw new Win32Exception(errNo);
    }
}
finally
{
    if (hStdOutReadPipe != IntPtr.Zero)
    {
        w32.CloseHandle(hStdOutReadPipe);
    }
}

lib.PrintStandard("PrintStandard\n");

using (FileStream fs = new FileStream(hDupStdOutReadPipe, FileAccess.Read))
{
    StreamReader sr = new StreamReader(fs);
    txtMultiOutput.Text = sr.ReadToEnd();
}

----
最後にFileStreamをベースとしたStreamReaderで出力側のパイプを開こう
とすると画面のほうへ制御が返ってこなくなります。ReadToEnd()で
ブロックされているのか…。別にスレッドを作らないとダメでしょうか…。

もう一息なのか、外しているのか、まだ模索中なのでなんとも言えません。
> あるボタンのクリックイベントに以下のソースを記述しました。
パイプの作成や標準出力のリダイレクトは起動時に一回やっとくタイプの処理だと思います。

GetStdHandle を呼び出す意図がわかりません。あるタイミングでパイプによる標準出力のリダイレクトをやめたい、ってことなら GetStdHandle は意味があるでしょうけど。
DuplicateHandle は子プロセスに継承させる場合に必要な処理なので、読み書きとも自分自身のプロセスで使う場合は不要だと思います。

> 最後にFileStreamをベースとしたStreamReaderで出力側のパイプを開こう
> とすると画面のほうへ制御が返ってこなくなります。ReadToEnd()で
> ブロックされているのか…。別にスレッドを作らないとダメでしょうか…。
はい、ReadToEnd に限らず StreamReader は同期的に処理するので、バッファが空の間はブロックします。
読み取り用のワーカスレッドを作る必要があるでしょうね。
// FileStream レベルなら非同期も可能ですが、CreatePipe から作ったストリームに使えるかどうかは知りません。
■No23263に返信(Hongliangさんの記事)
> パイプの作成や標準出力のリダイレクトは起動時に一回やっとくタイプの処理だと思います。
> GetStdHandle を呼び出す意図がわかりません。
> DuplicateHandle は子プロセスに継承させる場合に必要な処理なので、読み書きとも自分自身のプロセスで使う場合は不要だと思います。
> 読み取り用のワーカスレッドを作る必要があるでしょうね。

このあたりを整理してやってみました(ソースは後ほど)。
結果はNo23213と全く同じになりました。Cosole.WriteLn()等使わないと出力されません。Console.SetOut()と同じことをパイプを使って表現したにすぎなかったようです。

以下の文書によると、Windowsアプリケーションは起動時にコンソールを持たず、標準入出力エラーハンドラーが存在しません。AllocConsole()を使用してハンドルをセットする先を作っても、printf()等のCランタイム出力ルーチンは無効なハンドルに出力し続けるようです。

http://support.microsoft.com/kb/105305/ja

解決はしていませんが、この件はクローズ…したほうがよさそうです…。
コンソールは標準出力の出力先の一つであって、その有無は標準出力とは直接は関係ありません。
Windows アプリケーションでは初期状態でコンソールを持たないので標準出力の出力先がないだけで、そこにパイプを継いでやればちゃんとそのパイプに出力されます。
というのを、調べもせず SetOut を勧めてしまったのに懲りて今回は実装してちゃんと動作確認しました。

考えられるのは、ReadLine で読み進めているけど printf で改行を出力していないとか、前述のとおり printf のバッファリングが行われているとか(リンク先のとおり、setvbuf を使用すれば回避できるみたいですね)でしょうか。
2008/10/31(Fri) 11:20:10 編集(投稿者)

それから、printf などは、初めて呼び出されたときに標準出力のハンドルを内部で保持し、以降それを使いまわすようです。
ですから、それらの初回呼び出しより前に SetStdHandle による標準出力の出力先変更を行う必要があります。

[08/10/31 11:20ごろ追記]
デバッガとか使ってるとこっちのMainに来る前にこっそり printf とかやられて乗っ取りようが無い可能性があるかもしれないな、とふと思いました。

DOBON.NET | プログラミング道 | プログラミング掲示板