Перенаправить стандартный вывод на консоль в приложении с графическим интерфейсом

У меня есть приложение на C++, которое я компилирую в режиме графического интерфейса и иногда запускаю его из консоли в целях отладки. Поскольку обычно он ничего не пишет в консоль, я подключил консоль и использовал freopen() для перенаправления стандартного вывода на консоль, используя следующий код:

#include <iostream>
#include <cstdio>
#include <windows.h>

int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    // Attach to the parent console
    if (AttachConsole(ATTACH_PARENT_PROCESS)) {
        // Redirect stdout and stderr to the console
        freopen("CONOUT$", "w", stdout);
        freopen("CONOUT$", "w", stderr);
    }
    std::cout << "print to stdout" << std::endl;
    std::cerr << "print to stderr" << std::endl;

    return 0;
}

Однако я бы хотел сделать это, используя WinAPI напрямую, а не freopen().

Я пытался использовать SetStdHandle() или следовать https://asawicki.info/news_1326_redirecting_standard_io_to_windows_console, но безуспешно, он ничего не печатает по сравнению с freopen, который работает.

Кроме того, когда программа, на которой работает терминал, плохо определяет, что она в данный момент работает, и если я нажимаю Enter, терминал обнаруживает и показывает мне подсказку вместо того, чтобы отправлять ее в программу. Это тоже можно исправить?

🤔 А знаете ли вы, что...
Язык C++ поддерживает метапрограммирование с использованием шаблонов.


1
140
2

Ответы:

Слишком долго для комментария, но я давно этого не делал и не уверен, что что-то пропустил. (Не стесняйтесь редактировать.)

Нет абсолютно ничего плохого в подключении консоли к процессу графического интерфейса в Windows. Вам просто нужно расставить все точки над «i».

AttachConsole() предназначен для подключения к существующему процессу-конхосту. Если консоль родительского процесса отсутствует, произойдет сбой. Если вы хотите просто открыть консоль, принадлежащую только вам, используйте AllocConsole(). Обе функции не будут работать, если вы уже подключены к консоли. Используйте FreeConsole(), чтобы сначала отключиться.

После подключения к консоли вам необходимо использовать глубокую магию Win32, чтобы правильно связать стандартные потоки с этой консолью. Вся важная магия заключена в этой console_init_c() рутине. Мы также стараемся не отбрасывать перенаправленные потоки, как бы странно это ни было запускать графический интерфейс с перенаправленным стандартным вводом-выводом.

(Хотя Q помечен C++, следующий — C, поэтому он работает как с C, так и с C++):

// Windows library code requires the following to
// be true in order to use the console functions.

#ifndef _WIN32_WINNT
  #define _WIN32_WINNT 0x0501
#elif _WIN32_WINNT < 0x0501
  #undef _WIN32_WINNT
  #define _WIN32_WINNT 0x0501
#endif

#include <windows.h>
#include <io.h>
#include <fcntl.h>

#ifndef __cplusplus
  #include <iso646.h>
  #include <stdbool.h>
  #include <stdio.h>
#else
  #include <ciso646>
  #include <cstdio>
#endif


static bool is_console_redirected( DWORD id )
{
  HANDLE h = GetStdHandle( id );      // (the handle is redirected if)
  return (h != INVALID_HANDLE_VALUE)  // the standard handle is already attached to something
      and (GetFileType( h ) != FILE_TYPE_CHAR);  // AND: file type is PIPE, DISK, etc
}


static bool console_init_c( DWORD id, FILE * stdfp, const char * mode )
{
  intptr_t h  = (intptr_t) GetStdHandle( id );
  int      fd = _open_osfhandle( h, _O_TEXT );  if (!fd)                 return false;
  FILE *   fp = _fdopen( fd, mode );            if (!fp) { _close( fd ); return false; }
  *stdfp = *fp;
  setvbuf( stdfp, NULL, _IONBF, 0 );  // stdin, stdout, and stderr are all unbuffered on Windows
  return true;
}


bool console_attach_pid( DWORD pid, bool is_detach_first )
//
// pid
//   0                      :: Attach to my own (new) console
//   ATTACH_PARENT_PROCESS  :: Attach to the parent process's console
//   (some process's PID)   :: Attach to the given process's console
//
// is_detach_first
//   false  :: Returns true/success if the process is already attached
//             to any console, regardless of the value of `pid`.
//   true   :: Use this if you wish to attempt to _change_ a console
//             attachment from (for example) the parent's to a new one.
//
{
  // Are any of the standard streams currently attached to a redirected stream?

  bool is_stdin_redirected  = is_console_redirected( STD_INPUT_HANDLE  );
  bool is_stdout_redirected = is_console_redirected( STD_OUTPUT_HANDLE );
  bool is_stderr_redirected = is_console_redirected( STD_ERROR_HANDLE  );

  // Attempt to connect to the desired console host

  if (is_detach_first)
    FreeConsole();

  BOOL ok = pid
    ? AttachConsole( pid )
    : AllocConsole();

  if (!ok)
    switch (GetLastError())
    {
      // Already attached to a console
      case ERROR_ACCESS_DENIED:
        return true;

      // No parent; try to create a new console
      case ERROR_INVALID_HANDLE:
      case ERROR_INVALID_PARAMETER:
        if (!AllocConsole())
          return false;

      // All other errors
      default:
          return false;
    }

  // Now for the Windows magic that makes C I/O work
  // (Read the docs for why we don't need to worry about
  //  managing resources beyond what we have done here.)
  
  bool success =
    (is_stdin_redirected  or console_init_c( STD_INPUT_HANDLE,  stdin,  "rt" )) and
    (is_stdout_redirected or console_init_c( STD_OUTPUT_HANDLE, stdout, "wt" )) and
    (is_stderr_redirected or console_init_c( STD_ERROR_HANDLE,  stderr, "wt" ));

  if (!success)
    FreeConsole();
      
  return success;
}

Я думаю, что некоторые дополнительные меры (с dup() и т. д.) могут потребоваться, если вы планируете напрямую играть с младшими файловыми дескрипторами 0, 1 и 2, но опять же, я не уверен (и не хочу возиться с этим прямо сейчас). Опять же, отредактируйте, если вам виднее).

Обратите внимание, что код технически UB, но это то, как это сделать в Windows.


Решено

Судя по ответам в комментариях:

Стандартные потоки c++ управляются внутри библиотеки времени выполнения c. SetStdHandle влияет на другие вызовы WinAPI, использующие дескриптор, но не на потоки среды выполнения c++. Вот почему мне нужно использовать freopen() или аналогичные функции из libc, чтобы изменить потоки времени выполнения c++.