一、如何?
列印調試資訊的方法有很多,最常用的是使用標準輸出裝置(如printf、cout等),也可以用OutPutDebugString輸出、用DebugView工具查看,還可以寫入記錄檔。如果程式運行需要記錄日誌(log),往往需要開啟個檔案,或許是寫入系統事件、用系統的事件檢視器查看。
應用程式列印調試資訊、日誌的方法往往是確定的,但如果是要編寫一個模組或者說組件,那樣的輸出資訊應該寫入哪裡呢?或者說程式本身對此也沒有明確需求的話,那該怎麼辦呢?
可喜的是一個進程的標準輸出是可以重新導向的,所以我建議把調試資訊直接打到標準輸出上,這樣代碼中可以統一使用cout或者printf,然後根據需要將標準輸出重新導向。
Linux中重新導向標準輸出就容易了,因為有強大的dup2函數。而對於Windows的重新導向,貌似往往是用於子進程的,在用CreateProcess建立子進程時設定子進程的標準輸出控制代碼。而我們想要的是重新導向自己這個進程的標準輸出,那個用不上。我在MSDN中也沒找到類似dup2的Win32 API函數,只有一個DuplicateHandle函數,這相當於linux中的dup,也用不上。
這裡順便提下,SetStdHandle是不能實現重新導向的。這個函數的功能是將某控制代碼指向標準裝置,並不能將標準裝置控制代碼重新導向到另外的控制代碼。
於是我就想到,Windows不是支援一部分POSIX標準的嗎。於是我找到了一個CRT的函數,叫_dup2,看起來是不是特眼熟,對了,這就是Windows中dup2的相容版本。
值得注意的是,_dup2以及與此相關的一系列CRT中的IO函數(如_read,_write)均以底線開頭,其餘與linux大致相同,其參數中所謂的檔案描述符與Win32中的控制代碼不一樣,檔案描述符實際上是控制代碼數組的索引,也就是說檔案描述符不能與控制代碼混用。比如0,1,2分別是標準輸入、標準輸出、標準錯誤的檔案描述符,但控制代碼值不是這樣確定的。檔案描述符不是Win32的概念,是POSIX中的概念。
_dup2用法與dup2大致相同,不多解釋,不瞭解的可以查閱dup2相關資料。下面講點應用。
二、如何應用於調試
寫一個模組時,我們可以直接用cout/printf來作調試。但是如果這個模組用於圖形介面或許是系統服務呢?這時標準輸出看不到了,我們可以用OutPutDebugString函數和DebugView這樣的調試工具。這樣就帶來一種選擇,而選擇往往是增加軟體複雜度的因素。所以我的想法是代碼中只用cout/printf,如果需要將其重新導向到調試工具中去。
如何?呢,用匿名管道和線程。
用一個pipe,標準輸出重新導向到其write端,然後建立一個線程,線程要做的就是從pipe的read端讀出資料後用OutPutDebugString輸出。
下面是實現代碼。
標頭檔是這樣子的,構造時重新導向,析構時解除:
namespace common {
// 將標準輸出重新導向到DebugView,保持對象存在即有效
class StdoutToDebugString {
public:
StdoutToDebugString();
~StdoutToDebugString();
private:
int fds_[2];
int orign_stdout_;
uintptr_t thread_handle_;
};
}
實現檔案:
#include <io.h>
#include <fcntl.h>
#include <stdio.h>
#include <process.h>
#include <Windows.h>
#include "StdoutRedirect.h"
using namespace common;
const int kBufferSize = 4096;
unsigned __stdcall RedirectThreadProc(void* param) {
int pipe_read = (int)param;
char buf[kBufferSize];
int bytes_read;
do {
bytes_read = ::_read(pipe_read, buf, kBufferSize);
buf[bytes_read] = 0;
::OutputDebugString(buf);
} while (bytes_read);
return 0;
};
StdoutToDebugString::StdoutToDebugString() {
::_pipe(fds_, kBufferSize, _O_TEXT);
orign_stdout_ = ::_dup(_fileno(stdout));
::_dup2(fds_[1], _fileno(stdout));
thread_handle_ = ::_beginthreadex(NULL, 0, RedirectThreadProc, (void*)fds_[0], 0, NULL);
::CloseHandle((HANDLE)thread_handle_);
}
StdoutToDebugString::~StdoutToDebugString() {
::_dup2(orign_stdout_, _fileno(stdout));
}
測試代碼:
#include <iostream>
#include <Windows.h>
#include "../Common/StdoutRedirect.h"
using namespace std;
using namespace common;
void main() {
StdoutToDebugStringredirect;
cout<< "hello" << endl;
::system("pause");
}
運行時可以看到控制台上並沒有列印出hello,而在DebugView中可以看到。
三、未解決的問題?
因為_read是阻塞的,我沒有辦法能安全地結束掉那個線程,所以我無法在析構時等待線程結束。如果析構中用_close關閉檔案描述符的話,當對象已經析構而線程依然在跑,_read會引發assert導致崩潰。
不過在一般情況下,在main的開始構造這樣一個對象來實現重新導向,一直維持到main結束,是沒有問題的。
對於此問題,還望牛人指點。