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

У меня есть приложение на 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, терминал обнаруживает и показывает мне подсказку вместо того, чтобы отправлять ее в программу. Это тоже можно исправить?

ИДК, почему кто-то захотел добавить тег Java. Это тоже не код C.

Weather Vane 27.08.2024 20:39

@WeatherVane у всех форм спама есть одна причина — привлечь больше внимания.

0___________ 27.08.2024 20:41

В статье, которую вы цитируете, не используется SetStdHandle.

john 27.08.2024 20:44

Я использую функции winapi AttachConsole() или AllocConsole(), а затем WriteConsole().

Weather Vane 27.08.2024 20:48

Я предпочитаю использовать /subsystem:console в режиме отладки и /subsystem:windows в выпуске. Хотя с учетом вышесказанного я пишу приложения с графическим интерфейсом Qt, а не непосредственно Winapi.

drescherjm 27.08.2024 21:03

@WeatherVane, тебе следует больше беспокоиться о том, как было одобрено редактирование тега java. Я вижу в этом хороший потенциал аудита.

user4581301 27.08.2024 21:06

@user4581301 user4581301 это не было одобрено, но рецензент предложил внести изменения, которые я отклонил.

Weather Vane 27.08.2024 21:08

@WeatherVane интересно. Страница истории изменений читается так, как будто это было полностью одобренное редактирование. Временная шкала в этом отношении не стала более ясной. Если это ошибка, то ее не стоит исправлять перед лицом всего остального, требующего хорошей доработки.

user4581301 27.08.2024 21:10

> Короче говоря: не пытайтесь повторно подключиться к консоли. Но мне нужно увидеть стандартный вывод в консоли. Ответ: В более широком смысле у меня есть приложение с графическим интерфейсом, которое мне иногда приходится отлаживать в рабочей среде на стороне пользователя. На самом деле это Rust вызывает привязки C++. Я завишу от библиотек С++, которые выводят информацию об ошибках в стандартный вывод. Используя метод Attach, я могу видеть ошибки этих библиотек даже в рабочей среде, просто запустив приложение с консоли.

anonymous 27.08.2024 21:15

@user4581301 user4581301 похоже, что оно было одобрено в ту же секунду, когда я отклонил и заменил редактирование, то есть за долю до этого.

Weather Vane 27.08.2024 21:16

@anonymous, поскольку у вас есть что-то, записывающее данные в объект stdout, недостаточно просто назначить новый дескриптор вызывающего процесса с помощью SetStdHandle(), вам нужно использовать freopen() для повторной инициализации объекта stdout, чтобы он фактически использовал новый дескриптор.

Remy Lebeau 27.08.2024 21:34

@RemyLebeau Можно ли имитировать freopen с помощью WinAPI?

anonymous 27.08.2024 21:35

@anonymous нет, потому что объект stdout не является частью Win32 API, он принадлежит библиотеке времени выполнения C. Итак, после изменения дескриптора на уровне Win32 вам необходимо обновить библиотеку времени выполнения C, чтобы она знала о новом дескрипторе. Библиотека времени выполнения вызывает GetStdHandle() только один раз при запуске, если вы не прикажете ей обновиться.

Remy Lebeau 27.08.2024 21:40

@RemyLebeau Спасибо, теперь стало намного яснее. Как насчет имитации всего поведения WinAPI для достижения аналогичного результата? Я надеюсь, что хотя бы это возможно с WinAPI.

anonymous 27.08.2024 21:42

@anonymous, какое именно поведение имитировать?

Remy Lebeau 27.08.2024 21:44

Обычно достаточно freopen("CONOUT$", "w", stdout); и std::ios::sync_with_stdio(). Требуется некоторая дополнительная магия, чтобы убедиться, что уже не выполняются перенаправления, иначе вы окажетесь в неправильных местах, пытаясь повторно привязаться к CONOUT$, поскольку закрытие предыдущих дескрипторов выходит из строя. (Поэтому AllocConsole() предпочтительнее AttachConsole()).

Ext3h 27.08.2024 21:47

@RemyLebeau Программа, скомпилированная в графическом режиме (subsystem/windows). Обычно, когда я запускаю ее из консоли, стандартный вывод не отображается. Я хочу показать этот вывод, если программа запускается с консоли (и AttachConsole преуспевает)

anonymous 27.08.2024 21:48

много комментариев TL:DR, мой 2c: используйте файл журнала, например. spdlog. тогда ты стал лучше контролировать

AndersK 27.08.2024 21:49

@Ext3h Я сделал то же самое во фрагменте, прикрепленном к вопросу. Я пытаюсь сделать это без freopen(), а с помощью WinAPI или чего-то подобного. Поскольку я делаю это из Rust и предпочитаю не использовать крейты libc (библиотеку, которая является привязкой к libc)

anonymous 27.08.2024 21:51

@AndersK У меня нет контроля над библиотеками C++, от которых я завишу, и мне нужно получить от них стандартный вывод

anonymous 27.08.2024 21:52

«Как насчет того, чтобы имитировать все поведение WinAPI для достижения аналогичного результата? Я надеюсь, что, по крайней мере, это возможно с WinAPI» - Конечно, это возможно. Однако вам придется вызвать WriteConsole (или WriteFile). Вы не можете использовать потоки ввода-вывода C или C++.

IInspectable 27.08.2024 21:54

@anonymous Вы не сможете сделать это без среды выполнения libc, без нее вы даже не узнаете дескриптор, связанный с stdout, и вы также не сможете обойти среду выполнения C++, поскольку для std::ios::sync_with_stdio() тоже нет бэкдора . Итак, что бы вы ни собирались делать, это все равно не будет работать с существующими сторонними библиотеками C++.

Ext3h 27.08.2024 21:56

@llnspectable См. предыдущий комментарий «У меня нет контроля над библиотеками C++, от которых я завишу, и мне нужно получить от них стандартный вывод»

anonymous 27.08.2024 21:56

@anonymous, тогда вы застряли в использовании freopen() и sync_with_stdio, поэтому существующие библиотеки продолжат работать правильно и фактически записывают данные в ваш новый стандартный вывод.

Remy Lebeau 27.08.2024 22:01

@RemyLebeau Почему использование SetStdHandle(STD_ERROR_HANDLE, ...) с дескриптором консоли не должно работать, а freopen() работает?

anonymous 27.08.2024 22:06

@anonymous Как я уже объяснял ранее, stdout/stderr инициализируются один раз при запуске. Это означает, что в вашем случае, когда они вызывают GetStdHandle() во время настройки приложения, он не вернет действительный дескриптор, который они могли бы использовать, поскольку к процессу еще не подключена консоль. ПОСЛЕ того, как вы позвоните SetStdHandle(), чтобы назначить новый дескриптор, вам необходимо повторно инициализировать stdout/stderr с помощью frepoen(), чтобы они затем снова позвонили GetStdHandle(), чтобы получить обновленный дескриптор и начать его использовать.

Remy Lebeau 27.08.2024 22:20

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

anonymous 27.08.2024 22:32

@anonymous, тогда, возможно, следует добавить это в вопрос - что вы используете сторонние библиотеки, в которых вам нужна эта запрошенная функциональность.

AndersK 30.08.2024 17:15
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
28
140
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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.

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

Andrew Henle 28.08.2024 16:55

«следующее C, поэтому оно работает как с C, так и с C++» — я жду, пока лингвисты-юристы научат вас, что «C и C++ — очень разные языки», сразу после того, как они вас смолили, обмазали и сожгли на ставка. А если серьезно, здесь не хватает многих объяснений, в первую очередь того, как среда выполнения языковой поддержки должна быть замешана в этой шутке. Это не «какая-то глубокая магия Win32». Действительно, Win32 ни в чем из этого не участвует.

IInspectable 28.08.2024 16:57

@AndrewHenle Да, документы ясно дают понять, что вам не следует fclose() делать такие вещи.

Dúthomhas 28.08.2024 17:04

@IInspectable ??? Разные языки, которые часто пересекаются, как в этом случае. И _open_osfhandle() определенно является магией Win32. 🙂

Dúthomhas 28.08.2024 17:07

Все, что имеет начальное подчеркивание, определенно не является символом в Win32 API. Действительно, _open_osfhandle объявлен в <io.h> (который делегирует <corecrt_io.h>), части Universal CRT. Он поставляется как часть ОС, но касается поддержки языка C.

IInspectable 28.08.2024 17:30

@Dúthomhas Я также помню, что двоичный/текстовый режим важен как для _open_osfhandle(), так и для _fdopen(). Я думаю, что _O_TEXT в вашем вызове _open_osfhandle( h, _O_TEXT ) может вызвать проблемы для двоичных потоков.

Andrew Henle 28.08.2024 17:44

@Dúthomhas Для простоты в примерах я игнорирую проверку ошибок и обработку перенаправления. Я закомментировал freopen и вместо этого использовал console_init_c(), но это не работает. Pastebin.com/cwjGf813 Кстати, согласно ответам в предыдущих комментариях, это невозможно без использования freopen(), который меняет внутренний дескриптор libc

anonymous 28.08.2024 18:38

@AndrewHenle Если он подключен к консоли, то это текстовый поток. Только перенаправленные данные могут быть двоичными.

Dúthomhas 28.08.2024 18:38

@anonymous Предлагаемая вам публичная функция: attach_console_pid(). Если вы просто вырежете и вставите его кусочки, и особенно если вы не проверите условия ошибки, то, скорее всего, что-то пойдет не так.

Dúthomhas 28.08.2024 18:40

@Dúthomhas Я не вижу функции attach_console_pid(). Я вижу console_attach_pid() но ты не звонил

anonymous 28.08.2024 18:43

Извините, опечатка. Я тоже не определился main(). Целью было не переписать вашу программу, а дать вам небольшую библиотечную функцию для использования в вашем коде.

Dúthomhas 28.08.2024 18:45

@Dúthomhas Вот почему я попытался заменить freopen(), как вы можете видеть в добавленной мной ссылке. Но по какой-то причине это не работает. Может быть, утверждение, что без freopen() нельзя, правильное

anonymous 28.08.2024 19:26
Ответ принят как подходящий

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

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

Другие вопросы по теме