2010-07-07 22 views
4

我有一個C++應用程序,它動態加載插件DLL。該DLL通過std :: cout和std :: wcout發送文本輸出。基於Qt的UI必須抓取所有來自DLL的文本輸出並顯示它。 由於運行時庫差異,DLL可能具有不同的cout/wcout實例,因此使用流緩衝區替換的方法並不完全可行。因此,我已經應用Windows特定STDOUT重定向如下:從重定向的STDOUT(C++,Win32 API,Qt)讀取Unicode

StreamReader::StreamReader(QObject *parent) : 
    QThread(parent) 
{ 
    // void 
} 

void StreamReader::cleanUp() 
{ 
    // restore stdout 
    SetStdHandle (STD_OUTPUT_HANDLE, oldStdoutHandle); 

    CloseHandle(stdoutRead); 
    CloseHandle(stdoutWrite); 
    CloseHandle (oldStdoutHandle); 

    hConHandle = -1; 

    initDone = false; 
} 

bool StreamReader::setUp() 
{ 

    if (initDone) 
    { 
     if (this->isRunning()) 
      return true; 
     else 
      cleanUp(); 
    } 

    do 
    { 
     // save stdout 
     oldStdoutHandle = ::GetStdHandle (STD_OUTPUT_HANDLE); 

     if (INVALID_HANDLE_VALUE == oldStdoutHandle) 
      break; 

     if (0 == ::CreatePipe(&stdoutRead, &stdoutWrite, NULL, 0)) 
      break; 

     // redirect stdout, stdout now writes into the pipe 
     if (0 == ::SetStdHandle(STD_OUTPUT_HANDLE, stdoutWrite)) 
      break; 

     // new stdout handle 
     HANDLE lStdHandle = ::GetStdHandle(STD_OUTPUT_HANDLE); 

     if (INVALID_HANDLE_VALUE == lStdHandle) 
      break; 

     hConHandle = ::_open_osfhandle((intptr_t)lStdHandle, _O_TEXT); 
     FILE *fp = ::_fdopen(hConHandle, "w"); 

     if (!fp) 
      break; 

     // replace stdout with pipe file handle 
     *stdout = *fp; 

     // unbuffered stdout 
     ::setvbuf(stdout, NULL, _IONBF, 0); 

     hConHandle = ::_open_osfhandle((intptr_t)stdoutRead, _O_TEXT); 

     if (-1 == hConHandle) 
      break; 

     return initDone = true; 

    } while(false); 


    cleanUp(); 

    return false; 
} 

void StreamReader::run() 
{ 
    if (!initDone) 
    { 
     qCritical("Stream reader is not initialized!"); 
     return; 
    } 

    qDebug() << "Stream reader thread is running..."; 

    QString s; 
    DWORD nofRead = 0; 
    DWORD nofAvail = 0; 

    char buf[BUFFER_SIZE+2] = {0}; 

    for(;;) 
    { 
     PeekNamedPipe(stdoutRead, buf, BUFFER_SIZE, &nofRead, &nofAvail, NULL); 

     if (nofRead) 
     { 
      if (nofAvail >= BUFFER_SIZE) 
      { 
       while (nofRead >= BUFFER_SIZE) 
       { 
        memset(buf, 0, BUFFER_SIZE); 
        if (ReadFile(stdoutRead, buf, BUFFER_SIZE, &nofRead, NULL) 
         && nofRead) 
        { 
         s.append(buf); 
        } 
       } 
      } 
      else 
      { 
       memset(buf, 0, BUFFER_SIZE); 
       if (ReadFile(stdoutRead, buf, BUFFER_SIZE, &nofRead, NULL) 
        && nofRead) 
       { 
        s.append(buf); 
       } 

      } 

      // Since textReady must emit only complete lines, 
      // watch for LFs 
      if (s.endsWith('\n')) // may be emmitted 
      { 
       emit textReady(s.left(s.size()-2)); 
       s.clear(); 
      } 
      else // last line is incomplete, hold emitting 
      { 
       if (-1 != s.lastIndexOf('\n')) 
       { 
        emit textReady(s.left(s.lastIndexOf('\n')-1)); 
        s = s.mid(s.lastIndexOf('\n')+1); 
       } 
      } 

      memset(buf, 0, BUFFER_SIZE); 
     } 
    } 

    // clean up on thread finish 
    cleanUp(); 
} 

然而,這種方案似乎有障礙物 - C運行時庫,其是區域設置相關。因此,發送到wcout的任何輸出都不會到達我的緩衝區,因爲C運行時會以UTF-16編碼的字符串中存在的不可打印的ASCII字符截斷字符串。調用setlocale()演示了C運行時執行字符串重新編碼。 setlocale()對我來說沒有任何幫助,因爲沒有關於文本的語言或語言環境的知識,因爲插件DLL從系統外部讀取,並且可能會混合使用不同的語言。 在給出了一個N想法後,我決定放棄這個解決方案,並恢復到cout/wcout緩衝區替換,並且要求DLL調用初始化方法,原因有兩個:UTF16不傳遞到我的緩衝區,然後解決問題編碼在緩衝區中。然而,我仍然對是否有辦法通過C運行時將UTF-16字符串轉換爲「原樣」管道,而無需依賴於區域的轉換?

p.s.任何關於cout/wcout重定向到UI的建議,而不是以上提及的兩種方法都是受歡迎的:)

提前致謝!

+0

您說得對,問題在於運行時如何處理(根據標準的要求)'std :: wcout'。如果發送者和接收者都使用普通的Windows API函數,這將是最簡單的方法。 – Philipp 2010-07-07 10:21:12

+0

謝謝菲利普!由於遺留問題插件使用cout/wcout,我必須讓它們工作。 – 2010-07-07 14:31:41

+0

如果他們寫入'cout',你可能會試圖強制他們使用UTF-8,但是我不知道如何處理'wcout',因爲標準要求對某些本地編碼進行編碼,這通常是一個過時的8-位編碼。 – Philipp 2010-07-09 20:47:29

回答

0

我不知道這是否可能,但也許你可以在一個單獨的進程中啓動DLL,並捕獲該進程的輸出與Windows等效的pipe(不管那是什麼,但Qt的QProcess應該照顧那對你)。這與Firefox不使用進程插件類似(在3.6.6中爲默認設置,但已使用64位Firefox和32位Flash插件完成了一段時間)。你必須想出一些方法在獨立進程中與DLL進行通信,例如共享內存,但它應該是可能的。不一定很漂亮,但可能。

+0

這是一個有趣的方法!我不確定它在我的特定情況下是否實用,但總的想法很有趣。我一定會因好奇而嘗試。 – 2010-07-13 07:33:47

+0

@Sergei:我想你仍然會遇到插件的wcout在嘗試輸出非ascii字符時會窒息的問題。 – 2010-07-13 22:57:55

0

嘗試:

std::wcout.imbue(std::locale("en_US.UTF-8")); 

這是流特有的,而且比使用全局C庫setlocale()更好。

但是,您可能必須調整語言環境名稱以適應您的運行時支持的內容。

+0

這裏至少存在的問題是語言環境未知。字符串是UCS2編碼的,但區域設置是不可預知的。這就是爲什麼整個地區的方法是不可接受的。不過感謝提示,我將檢查如何將locale設置爲std :: wcout影響輸出。 – 2010-07-13 07:31:21

+0

@Sergei Eliseev:實際上,它根本不依賴於用戶的語言環境,因爲語言環境是通過名稱顯式構建的,並且只與這一個流關聯。其他流可以有其他語言環境。恕我直言,遠離全球的語言環境是C++ std lib合適的重要之一。 – 2010-07-13 18:10:17

+0

@Sergei Eliseev:我終於借用了一臺Windows機器來試用,我發現Windows(至少是XP)不支持在本地名稱中使用UTF-8代碼頁。 IOW,std :: locale(「English_USA.UTF-8」)會拋出std :: runtime_error(「bad locale name」),而std :: locale(「English_USA.1252」)正常。我在網上查了一下,我認爲這是因爲UTF-8每個Unicode字符可以產生兩個以上的字節。 所以,我的解決方案在Windows上不起作用,不幸的是,即使它在Linux上也是如此。對於那個很抱歉。我將來會在Windows上檢查一些事情,而不是做出假設。 – 2010-07-13 21:14:34

1

這裏的問題是,代碼轉換從wchar_tchar被完全完成的插件DLL內,以任何cout/wcout實現它碰巧使用(這就像你說的可能不一樣主應用程序正在使用的一個)。因此,讓它行爲不同的唯一方法是以某種方式攔截該機制,例如用streambuf替換。

但是,正如你暗示的那樣,你在主應用程序中編寫的任何代碼都不一定與DLL使用的庫實現兼容。例如,如果您在主應用程序中實現流緩衝區,則它不一定會使用與DLL中的流緩衝區相同的ABI。所以這是有風險的。

我建議你實現一個使用與插件相同的C++庫版本的包裝器DLL,因此它保證是兼容的,並且在這個包裝器DLL中,在cout/wcout中進行必要的干預。它可以動態加載插件,因此可以使用任何使用該庫版本的插件重用。或者,您可以創建一些可以針對每個插件專門編譯的可重用源代碼,從而生成每個插件的清理版本。

一旦封裝了DLL,您可以將流緩衝區替換爲cout/wcout,這樣可以將數據保存到內存中,因爲我認爲您最初正在計劃,並且根本不必混淆文件句柄。 PS:如果你需要製作一個可以轉換成UTF-8和從UTF-8轉換的wstream,那麼我建議使用Boost的utf8_codecvt_facet作爲一個非常乾淨的方式來做到這一點。它很容易使用,並且文檔具有示例代碼。 (在這種情況下,您將不得不針對插件正在使用的庫版本編譯Boost版本,但不是在一般情況下)。