パソコンでプログラミングしよう ウィンドウズC++プログラミング環境の構築
1.6.3.6(15)
ソースコード(Code::BlocksのToolsメニュー)

Code::Blocksの[Tool]メニューおよび[Tools+]メニューによる外部ツール起動時挙動をソースコードから解析する。

ダウンロードリンク

ソースコード([Tool]による起動)(抜粋)

外部ツールの起動

外部ツールはToolsManager::OnToolClick(sdk\toolsmanager.cpp:372)を介しToolsManager::Execute(sdk\toolsmanager.cpp:81)が起動する。

bool ToolsManager::Execute(const cbTool* tool)
{
if (m_pProcess)
{
cbMessageBox(_("Another tool is currently executing.\n"
"Please allow for it to finish before launching another tool..."),
_("Error"), wxICON_ERROR);
return false;
}
...
wxString cmdline;
wxString cmd = tool->GetCommand();
wxString params = tool->GetParams();
wxString dir = tool->GetWorkingDir();
...
if (tool->GetLaunchOption() == cbTool::LAUNCH_NEW_CONSOLE_WINDOW)
{
...
#define CONSOLE_RUNNER "cb_console_runner.exe"
wxString baseDir = ConfigManager::GetExecutableFolder();
if (wxFileExists(baseDir + wxT("/" CONSOLE_RUNNER)))
cmdline << baseDir << wxT("/" CONSOLE_RUNNER " ");
}
if (!cmdline.Replace(_T("$SCRIPT"), cmd << _T(" ") << params))
// if they didn't specify $SCRIPT, append:
cmdline << cmd;
...
bool pipe = true;
int flags = wxEXEC_ASYNC;
switch (tool->GetLaunchOption())
{
case cbTool::LAUNCH_NEW_CONSOLE_WINDOW:
pipe = false; // no need to pipe output channels...
break;
case cbTool::LAUNCH_VISIBLE:
case cbTool::LAUNCH_VISIBLE_DETACHED:
flags |= wxEXEC_NOHIDE;
pipe = false;
break;
case cbTool::LAUNCH_HIDDEN: // fall-through
default:
break; // use the default values of pipe and flags...
}
if (tool->GetLaunchOption() == cbTool::LAUNCH_VISIBLE_DETACHED)
{
int pid = wxExecute(cmdline, flags);
...
}
else
{
m_pProcess = new PipedProcess(&m_pProcess, this, idToolProcess, pipe, dir);
m_Pid = wxExecute(cmdline, flags, m_pProcess);
...
}
return true;
} // end of Execute

外部ツールはwxWidgetsライブラリのwxExecute関数で子プロセスとして起動する。

long wxExecute (const wxString &command, int flags=wxEXEC_ASYNC, wxProcess *callback=NULL, const wxExecuteEnv *env=NULL)

commandは外部ツールを起動するコマンド文字列(コマンド名とコマンド実引数)、flagsは起動オプションフラグ、callbackはwxProcessインスタンスポインタで外部ツール終了時処理と標準入出力リダイレクトを担当する。callbackがNULLあるいはリダイレクトしない場合は外部ツールは表示で起動し、リダイレクトする場合は非表示で起動する。標準入出力を持たないデスクトップアプリケーションを外部ツールとしてもこれに従う。この動作はフラグにwxEXEC_SHOW_CONSOLE(=wxEXEC_NOHIDE)またはwxEXEC_HIDE_CONSOLEを加えることでオーバーライトできる。

ToolsManager::OnToolClickは最初にPipedProcessインスタンスポインタであるメンバ変数m_pProcessを検査し、これがNULLでない場合はメッセージループにアタッチされた外部ツールが起動済みとして新たな外部ツールを起動せずリターンする。NULLの場合は起動オプションを示す列挙値の定める実引数でwxExecute関数をコールして外部ツールを起動する。

オプション 列挙値 command flags callback
Launch tool in a new console window and wait for a keypress when done LAUNCH_NEW_CONSOLE_WINDOW cb_console_runner.exe cmd params wxEXEC_ASYNC PipedProcess(pipe=false)
Launch tool hidden with standard output redirect LAUNCH_HIDDEN cmd params wxEXEC_ASYNC PipedProcess(pipe=true)
Launch tool visible (without output redirection) LAUNCH_VISIBLE cmd params wxEXEC_ASYNC|wxEXEC_NOHIDE PipedProcess(pipe=false)
Launch tool visible detached (without output redirection) LAUNCH_VISIBLE_DETACHED cmd params wxEXEC_ASYNC|wxEXEC_NOHIDE NULL

commandにおいてcmdは外部ツールのコマンド名、paramsはコマンド実引数とする。cb_console_runner.exe(tools\ConsoleRunner\main.cpp)はCode::Blocksに付属するコンソールアプリケーションで、実引数として与えられる外部ツール起動文字列を子プロセスとして起動し終了後にキー入力待ち一時停止する。メッセージループに外部ツールをアタッチする場合、wxProcessクラスを継承するPipedProcessクラス(include\pipedprocess.h:18)の新たなインタンスを構築し、m_pProcessにそのポインタを代入してwxExecute仮引数のcallbackへ渡す。外部ツールが終了するとPipedProcess::OnTerminate(sdk\pipedprocess.cpp:208)が終了イベントを送出し、ハンドラToolsManager::OnToolTerminated(sdk\toolsmanager.cpp:379)がm_pProcessにNULLを代入する(処理イベントはEVT_END_PROCESSではなくカスタムなのでインスタンスは自己破棄する)。PipedProcessコンストラクタの仮引数pipeにtrueを与えると、wxProcess::Redirectをコールして標準入出力リダイレクトを可能とする。従ってLAUNCH_HIDDENのみリダイレクトしデフォルトで外部ツールは非表示起動となり、その他はデフォルトで表示起動なのでwxEXEC_NOHIDEは冗長である。

これで以下の全ての動作が説明できる。出力先がコマンドプロンプトの場合は新たに表示するコマンドプロンプトへ出力する。

オプション アプリケーション 出力先 表示 メッセージループ ツール終了時
Launch tool in a new console window and wait for a keypress when done コンソール コマンドプロンプト - アタッチ 一時停止
デスクトップ コマンドプロンプト メインウィンドウ
Launch tool hidden with standard output redirect コンソール [Logs & others|Code::Blocks] - アタッチ 終了
デスクトップ - 非表示
Launch tool visible (without output redirection) コンソール コマンドプロンプト - アタッチ 終了
デスクトップ - メインウィンドウ
Launch tool visible detached (without output redirection) コンソール コマンドプロンプト - デタッチ 終了
デスクトップ - メインウィンドウ

[Logs & others|Code::Blocks]ウィンドウへの出力

[Logs & others|Code::Blocks]ウィンドウへの出力について説明を追加する。

PipedProcessクラスはアイドルイベントハンドラPipedProcess::OnIdle(sdk\pipedprocess.cpp:229)からPipedProcess::HasInput(sdk\pipedprocess.cpp:173)をコールする(ソースコード上は定期タイマーハンドラ利用の痕跡も認められるが実行化されていない)。

void PipedProcess::OnIdle(cb_unused wxIdleEvent& event)
{
while ( HasInput() )
;
}
bool PipedProcess::HasInput()
{
if (IsErrorAvailable())
{
cbTextInputStream serr(*GetErrorStream());
wxString msg;
msg << serr.ReadLine();
CodeBlocksEvent event(cbEVT_PIPEDPROCESS_STDERR, m_Id);
event.SetString(msg);
event.SetX(m_Index);
wxPostEvent(m_Parent, event);
return true;
}
if (IsInputAvailable())
{
cbTextInputStream sout(*GetInputStream());
wxString msg;
msg << sout.ReadLine();
CodeBlocksEvent event(cbEVT_PIPEDPROCESS_STDOUT, m_Id);
event.SetString(msg);
event.SetX(m_Index);
wxPostEvent(m_Parent, event);
return true;
}
return false;
}

wxProcess::IsInputAvailableあるいはwxProcess::IsErrorAvailableで出力有無を確認し、必要ならcbEVT_PIPEDPROCESS_STDOUTあるいはcbEVT_PIPEDPROCESS_STDERRを送出する。このイベントはToolsManager::OnToolStdOutput(sdk\toolsmanager.cpp:392)あるいはToolsManager::OnToolErrOutput(sdk\toolsmanager.cpp:397)が捉えログウィンドウ([Logs & others|Code::Blocks]ウィンドウ)へ出力される。

void ToolsManager::OnToolStdOutput(CodeBlocksEvent& event)
{
Manager::Get()->GetLogManager()->Log(_T("stdout> ") + event.GetString());
}
void ToolsManager::OnToolErrOutput(CodeBlocksEvent& event)
{
Manager::Get()->GetLogManager()->Log(_T("stderr> ") + event.GetString());
}

ソースコード([Tools+]による起動)(抜粋)

外部ツールの起動

外部ツールはToolsPlus::OnRunTarget(plugins\contrib\ToolsPlus\ToolsPlus.cpp:260)が起動する。

void ToolsPlus::OnRunTarget(wxCommandEvent& event)
{
...
if (windowed)
{
...
wxArrayString astr;
m_shellmgr->LaunchProcess(commandstr, consolename, _("Piped Process Control"), astr);
ShowConsole();
} else if (console)
{
...
#define CONSOLE_RUNNER "cb_console_runner.exe"
wxString baseDir = ConfigManager::GetExecutableFolder();
if (wxFileExists(baseDir + wxT("/" CONSOLE_RUNNER)))
cmdline << baseDir << wxT("/" CONSOLE_RUNNER " ");
cmdline<<commandstr;
if (!wxExecute(cmdline))
cbMessageBox(_("Command Launch Failed: ")+commandstr);
}
else
{
if (!wxExecute(commandstr))
cbMessageBox(_("Command Launch Failed: ")+commandstr);
}
...
}

起動オプションで分岐フラグが設定され処理が選択される。windowed分岐以外では適切な実引数をcommandへ与えwxExecute関数をコールして外部ツールを起動する。windowed分岐では[Tool Output]ウィンドウ管理クラスShellManagerのインスタンスポインタであるメンバ変数m_shellmgrを用いてShellManager::LaunchProcess(contrib\ToolsPlus\ShellCtrlBase.cpp:136)をコールする。

long ShellManager::LaunchProcess(const wxString &processcmd, const wxString &name, const wxString &type, const wxArrayString &options)
{
int id=wxNewId();
ShellCtrlBase *shell=GlobalShellRegistry().CreateControl(type,this,id,name,this);
...
long procid=shell->LaunchProcess(processcmd,options);
...
m_nb->InsertPage(m_nb->GetPageCount()-1,shell,name);
m_nb->SetSelection(m_nb->GetPageCount()-2);
return procid;
}

processcmdは外部ツールを起動するコマンド文字列である。typeの実引数"Piped Process Control"によりShellCtrlBase抽象クラスを継承するPipedProcessCtrlインスタンスを作成しポインタをshellに保持する。shellメンバ関数PipedProcessCtrl::LaunchProcess(plugins\contrib\ToolsPlus\PipedProcessCtrl.cpp:92)をprocesscmdを実引数としてコールする。最後にshellをShellManagerクラスの管理するノートブックコントロール(m_nbメンバ変数)へ[Tool Output]ウィンドウとして挿入する。

long PipedProcessCtrl::LaunchProcess(const wxString &processcmd, const wxArrayString &/*options*/) // bool ParseLinks, bool LinkClicks, const wxString &LinkRegex
{
...
m_proc=new wxProcess(this,ID_PROC);
m_proc->Redirect();
m_procid=wxExecute(processcmd,wxEXEC_ASYNC,m_proc);
m_parselinks=true;//ParseLinks;
m_linkclicks=true;//LinkClicks;
m_linkregex=LinkRegexDefault; //LinkRegex;
if(m_procid>0)
{
m_ostream=m_proc->GetOutputStream();
m_istream=m_proc->GetInputStream();
m_estream=m_proc->GetErrorStream();
m_dead=false;
m_killlevel=0;
}
return m_procid;
}

wxProcessインスタンスを作成しwxProcess::Redirectをコールして入出力リダイレクトを可能とする。processcmdとwxProcessインスタンスポインタm_procを実引数としてwxExecute関数をコールして外部ツールを起動する。

以上から各起動オプションがwxExecute関数へ渡す値をまとめる。

オプション 分岐フラグ command flags callback
Tools Output Window windowed cmd prams wxEXEC_ASYNC wxProcess
Code::Blocks Console console cb_console_runner.exe cmd params wxEXEC_ASYNC NULL
Standard Shell 無し cmd params wxEXEC_ASYNC NULL

windowed分岐のみ標準入出力がリダイレクトされ外部ツールは非表示起動する。出力先がコマンドプロンプトの場合は新たに表示するコマンドプロンプトへ出力する。

オプション アプリケーション 出力先 表示 メッセージループ ツール終了時
Tools Output Window コンソール [Tool Output] - デタッチ 終了
デスクトップ - 非表示
Code::Blocks Console コンソール コマンドプロンプト - デタッチ 一時停止
デスクトップ コマンドプロンプト メインウィンドウ
Standard Shell コンソール コマンドプロンプト - デタッチ 終了
デスクトップ - メインウィンドウ

[Tool Output]ウィンドウへの出力

[Tool Output]ウィンドウへの出力について説明を追加し、特にその出力がASCII文字だけを正しく表示する理由を明らかにする。

ShellManagerクラスインスタンスは定期的なタイマーイベントハンドラShellManager::OnPollandSyncOutput(contrib\ToolsPlus\ShellCtrlBase.cpp:232)でノートブックコントロール(m_nb)の全てのShellCtrlBase抽象クラスの継承インスタンスを走査し出力を更新する。

void ShellManager::OnPollandSyncOutput(wxTimerEvent& /*te*/)
{
for(unsigned int i=0;i<m_nb->GetPageCount()-1;i++)
{
GetPage(i)->SyncOutput();
}
}

外部ツールの出力先として追加されたPipedProcessCtrlインスタンス(すなわち[Tool Output]ウィンドウ)はPipedProcessCtrl::SyncOutput(plugins\contrib\ToolsPlus\PipedProcessCtrl.cpp:152)で出力を更新する。

void PipedProcessCtrl::SyncOutput(int maxchars)
{
...
while(m_proc->IsInputAvailable())
{
char buf0[maxchars+1];
for(int i=0;i<maxchars+1;i++)
buf0[i]=0;
m_istream->Read(buf0,maxchars);
wxString m_latest=wxString::FromAscii(buf0);
long start,end;
start=m_textctrl->GetSelectionStart();
end=m_textctrl->GetSelectionEnd();
int pos=start>end?start:end;
bool move_caret=(pos>=m_textctrl->PositionFromLine(m_textctrl->GetLineCount())&&
(start==end));
m_textctrl->AppendText(m_latest);
if(move_caret)
{
m_textctrl->GotoLine(m_textctrl->GetLineCount());
}
if(oneshot)
break;
}
...
}

標準出力はm_textctrlメンバ変数に格納されたコントロールへ出力される。m_textctrlはPipedTextCtrlクラスインスタンスポインタであり、PipedTextCtrlはScintillaエディタコンポーネントをラップするwxScintillaクラス(wxScintillaはwxStyledTextCtrlの実装クラスでもある)を継承するのでユニコード文字列を表示できる。問題は出力を受け取ったバッファbuf0からコントロールへ出力するwxString型文字列m_latestへの変換にwxString::FromAsciiを用いていることで、これにより正しく表示できる文字がASCII文字に限定されてしまう。