Windows and Pipes

popen for Windows

This is a simple example of how you can execute a command line compiler (gcc, clang, cl, etc.) and have its output sent to a GUI window. This is kind of how many IDEs work. You can select the compiler from the drop-down box at the top and then compile or compile/link the code.

(Click on an image to view full size)

Building the code using GNU g++ (with warnings/errors). The command-line compiler (cl.exe, gcc.exe, clang.exe, etc.) is run as a child process and its output is redirected (piped) back to the GUI, which displays it in the bottom window in the tab labeled Compiler output. Clicking the mouse on the error message in the bottom window will highlight the line in the source code in the top window.



Building the code with GNU g++ (with no warnings/errors after fixing the problems above):


Building the code using Microsoft's compiler (with warnings/errors).


Building the code with Microsoft's compiler (with no warnings/errors after fixing the problems above):


Running (executing) the program. The executable is run as a child process and its output is redirected (piped) back to the GUI, which displays it in the bottom window in the tab labeled Run output. The time it took to run is also displayed in the output. The IDE gives you the option of displaying the output in the bottom window, or it will open a new console window and display the output there.


Running Doxygen on the (different source) code. The Doxygen warnings are displayed in the bottom window. The same behavior applies here, meaning, clicking on the warning in the bottom window highlights the code in the top window:


The HTML output from Doxygen is displayed (embedded) in the bottom window in the tab labeled Doxygen preview. The IDE gives you the choice of displaying the HTML in the embedded bottom window, or you can have it open the HTML in the (default) browser.


Here are some code snippets that demonstrate what's happening.

Compiling/linking:

const int MAX_CMD_LINE = 4096;

void TMainWindow::Compile(const char *filename, bool link)
{
  pgeMain->ActivePageIndex = 0;

  char cmdLine[MAX_CMD_LINE]; // compiler command line

    // Compile/Link or just compiling?
  char *lstr = "";
  if (!link)
    lstr = "-c";

    // If warnings are enabled (each compiler does it differently!)
  char *gwarns = "";
  char *mwarns = "";
  char *bwarns = "";
  if (chkEnableWarnings->Checked)
  {
    gwarns = "-Wall -Wextra -pedantic";
    bwarns = "-w";
    mwarns = "/W4";
  }

    // All compilers accept -O2!
  char *optimize = "";
  if (chkEnableOptimizations->Checked)
    optimize = "-O2";

    // Each compiler has its own command line format
  if (Compiler_ == cmpMICROSOFT)
    sprintf(cmdLine, "cl %s %s %s %s /EHsc", mwarns, optimize, lstr, filename);
  else if (Compiler_ == cmpGNU)
  {
    AnsiString s = ChangeFileExt(filename, ".exe");
    char *standard = "-ansi";
    if (chkCPP14->Checked)
      standard = "-std=c++14";
    sprintf(cmdLine, "g++ %s %s %s %s %s -o %s", gwarns, optimize, lstr, standard, filename, s.c_str() );
  }
  else if (Compiler_ == cmpCLANG)
  {
    AnsiString s = ChangeFileExt(filename, ".exe");
    char *standard = "-ansi";
    if (chkCPP14->Checked)
      standard = "-std=c++14";
    sprintf(cmdLine, "clang++ %s %s %s %s %s -o %s", gwarns, optimize, lstr, standard, filename, s.c_str() );
  }
  else if (Compiler_ == cmpBORLAND)
    sprintf(cmdLine, "bcc32 %s %s %s %s", bwarns, optimize, lstr, filename);
  else
    return; // unknown compiler (shouldn't happen)

  std::string output; // Compiler's output
  HANDLE process;     // Process' handle returned from CreatePipe

    // Start the compiler and capture its output
  HANDLE in = POPEN(cmdLine, &process);
  if (in)
  {
      // Read all of the compiler's output and put into a std::string
    while (1)
    {
      const int chunksize = 128; // How much to read each time
      char buffer[chunksize];    // Buffer to store output
      DWORD bytesRead;           // How much text was actually read

        // Read a chunk of text (output) from the compiler
      BOOL success = ReadFile(in, buffer, chunksize, &bytesRead, 0);

        // This will trigger either when there are no more bytes to read
        // or on error. (Need to handle the potential error at some point.)
      if (!success)
        break;

        // Add the chunk of text to the string
      for (DWORD i = 0; i < bytesRead; i++)
        output.push_back(buffer[i]);

        // Yield so the GUI is responsive (this could take a while)
      Application->ProcessMessages();
    }

      // Convert the buffer of characters to a list of strings
    TStringList *list = new TStringList();
    OutputToStrings(output, *list);
    mmoOutput->Lines->AddStrings(list);
    delete list;

      // Clean up
    CloseHandle(process);
    CloseHandle(in);
  }
  else
  {
    mmoOutput->Lines->Add(AnsiString("Failed! Command line: ") + cmdLine);
    // TODO: Handle the error
  }
}
Executing the program:
// Runs a command (cmdline) and redirects its output to the app
void TMainWindow::CaptureOutput(const char *cmdline)
{
    // Update GUI
  mmoRunOutput->Clear();
  Application->ProcessMessages();
  pgeMain->ActivePageIndex = 1;
  mmoRunOutput->Lines->Add("=== Running... ===");

    // Keep track of how long the run took
  double timediff;
  clock_t start = clock();

  HANDLE process;
  HANDLE in = POPEN(cmdline, &process);
  std::string output;
  if (in)
  {
      // Read all of the process' output
    while (1)
    {
      const int chunksize = 128; // How much to read each time
      char buffer[chunksize];    // Buffer to store output
      DWORD bytesRead;           // How much text was actually read

        // Read a chunk of text (output) from the compiler
      BOOL success = ReadFile(in, buffer, chunksize, &bytesRead, 0);

        // This will trigger either when there are no more bytes to read
        // or on error. (Need to handle the potential error at some point.)
      if (!success)
        break;

        // Add the chunk of text to the string
      for (DWORD i = 0; i < bytesRead; i++)
        output.push_back(buffer[i]);

        // Yield so the GUI is responsive (this could take a while)
      Application->ProcessMessages();
    }

      // Done reading
    CloseHandle(process);
    CloseHandle(in);

      // Elapsed time (rough estimate)
    timediff = (clock() - start) / (double)CLOCKS_PER_SEC;

      // Put the output into a list of strings
    TStringList *list = new TStringList();
    OutputToStrings(output, *list);
    mmoRunOutput->Lines->AddStrings(list);
    delete list;
  }
  else
  {
    //fprintf(stderr, "error: %s", errorMessage);
    timediff = (clock() - start ) / (double)CLOCKS_PER_SEC;
    Beep();
  }

    // Update GUI
  mmoRunOutput->Lines->Add("");
  mmoRunOutput->Lines->Add("=== Done. ===");

  char buf[100];
  sprintf(buf, "Elapsed time: %.3f ===", timediff);
  mmoRunOutput->Lines->Add(buf);
}
A Windows version of popen:
//---------------------------------------------------------------------------
// See MSDN article: "Creating a Child Process with Redirected Input and Output"
// for more information on this. There's no guarantee this link still works, but
// Google for it and you'll probably find something relevant.
//
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms682499%28v=vs.85%29.aspx
HANDLE TMainWindow::POPEN(const char* cmdLine, HANDLE *process)
{
  SECURITY_ATTRIBUTES sa;
  sa.lpSecurityDescriptor = NULL;
  sa.nLength = sizeof(sa);
  sa.bInheritHandle = TRUE; // need this for the pipe

  HANDLE pipeWrite; // Child will write to here
  HANDLE pipeRead;  // Parent will read from here

    // Create a new pipe for child to communicate with parent
  if (!CreatePipe(&pipeRead, &pipeWrite, &sa, 0 /* buffer size */))
    return NULL;

    // For CreateProcess
  PROCESS_INFORMATION info;
  STARTUPINFO si;
  ZeroMemory(&si, sizeof(si));
  si.cb = sizeof(si);
  si.wShowWindow = SW_HIDE;

    // No visible window, use redirected handles
  si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;

    // The child will write to the pipe instead of stdout.
  si.hStdOutput = pipeWrite;

    // We also want stderr to go to the pipe, since some compilers (e.g. g++)
    // send their warnings and errors to stderr.
  if (chkCaptureStderr->Checked)
    si.hStdError = pipeWrite;

    // Need to use wide-strings with CreateProcess (see MSDN docs)
  std::wstring wcstr(MAX_CMD_LINE, L' ');
  mbstowcs(&wcstr[0], cmdLine, MAX_CMD_LINE);

    // Create the child process
  BOOL success = CreateProcess
  (
    NULL,    // application name
    (wchar_t *)wcstr.c_str(),  // command line
    NULL,    // process security attributes
    NULL,    // thread attributes
    TRUE,    // inherit handles
    0,       // creation flags
    NULL,    // use parent's environment
    NULL,    // use parent's current directory
    &si,     // [in]
    &info    // [out] will be filled in
  );

    // Close the write end of the pipe since we (parent) are only going to read
    // from the child process.
  CloseHandle(pipeWrite);

    // If something went wrong (TODO: show error message)
  if (!success)
    return NULL;

  *process = info.hProcess; // for process management
  return pipeRead;          // caller reads from here
}