C++ - Two-way communication with child processes(using stdout/stdin)

 While programming, we often run other processes and receive the result value and proceed to the next task. 

I often use the subprocess module in Python to do this. 

In C/C++ programming, popen and system functions are often used. To receive the stdout output value of the child process, use the popen function, and when only the return value of the end of the child process is needed, use the system function.

#include <iostream>
#include <cstring>
using namespace std;
string exec_command(const char *cmd)
{
    string retstr("");
    char buffer[1024];

    FILE *pipe = popen(cmd, "r");
    if (pipe)
    {
        try
        {
            while (!feof(pipe))
            {
                if (fgets(buffer, sizeof(buffer), pipe) != NULL)
                    retstr += buffer;
            }
        }
        catch (...)
        {
        }
        pclose(pipe);
    }
    return retstr;
}
int main()
{
  string ret = exec_command("ls");
  cout << ret.c_str();
}

<test1.cpp>

Now let's compile and run the code above.

root@ubuntusrv:/usr/local/src/study/popen# g++ test1.cpp 
root@ubuntusrv:/usr/local/src/study/popen# ./a.out 
a.out
client
client.cpp
popen_test.cpp
test1.cpp

As you can see from the results, the screen output of the ls command is passed to the parent process. For reference, most of the system function returns the exit code of the child process.

But sometimes you need complete control over the child process. For example, there are times when it is necessary to run a child process, pass a string to stdin of the child process, the child process to work according to the contents of the string, and then pass the result to the parent process through stdout. The popen function can read the child process's stdout, but cannot write to stdin.

Communicate with child process stdout/stdin

First, let's compile the following code and create a child process for testing.

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <cstring>
int main()
{
  char in[256];

  while(1){
      in[0] = 0x00;
      std::cin.getline(in, 256);
      std::cout <<"from parent:" << in << std::endl; 
      if(strcmp(in, "exit") == 0) break;
  }
  return 0;
}

<client.cpp>

root@ubuntusrv:/usr/local/src/study/popen# g++ client.cpp -o client

And let's use the following Python code to communicate with the child process.

import sys
from subprocess import Popen, PIPE

proc = Popen(["/usr/local/src/study/popen/client"], stdin=PIPE, stdout=PIPE, bufsize=1)
proc.stdin.write("Hello Client\n".encode())
proc.stdin.flush()
line = proc.stdout.readline()
l = str(line.rstrip())
print ('From client:%s'%l)
proc.stdin.write("exit\n".encode())
proc.stdin.flush()
proc.communicate()

<popen_test.py>

root@ubuntusrv:/usr/local/src/study/popen# python3 popen_test.py 
From client:b'from parent:Hello Client'

You can see that Python's popen can be used with both stdin and stdout. Therefore, in Python, bi-directional communication with child processes is possible using the popen function of the subprocess module. However, C/C++'s popen function cannot communicate bi-directionally with the child process as described earlier.

There is a good example of implementing two-way communication with a child process in C++, so I imported it and modified some. The original code is

It can be downloaded from Konstantin Tretyakov's GitHub.

// 
// Example of communication with a subprocess via stdin/stdout
// Author: Konstantin Tretyakov
// License: MIT
//

#include <ext/stdio_filebuf.h> // NB: Specific to libstdc++
#include <sys/wait.h>
#include <unistd.h>
#include <iostream>
#include <memory>
#include <exception>
#include <cstring>
// Wrapping pipe in a class makes sure they are closed when we leave scope
class cpipe {
private:
    int fd[2];
public:
    const inline int read_fd() const { return fd[0]; }
    const inline int write_fd() const { return fd[1]; }
    cpipe() { if (pipe(fd)) throw std::runtime_error("Failed to create pipe"); }
    void close() { ::close(fd[0]); ::close(fd[1]); }
    ~cpipe() { close(); }
};


//
// Usage:
//   spawn s(argv)
//   s.stdin << ...
//   s.stdout >> ...
//   s.send_eol()
//   s.wait()
//
class spawn {
private:
    cpipe write_pipe;
    cpipe read_pipe;
public:
    int child_pid = -1;
    std::unique_ptr<__gnu_cxx::stdio_filebuf<char> > write_buf = NULL; 
    std::unique_ptr<__gnu_cxx::stdio_filebuf<char> > read_buf = NULL;
    std::ostream stdin;
    std::istream stdout;
    
    spawn(const char* const argv[], bool with_path = false, const char* const envp[] = 0): stdin(NULL), stdout(NULL) {
        child_pid = fork();
        if (child_pid == -1) throw std::runtime_error("Failed to start child process"); 
        if (child_pid == 0) {   // In child process
            dup2(write_pipe.read_fd(), STDIN_FILENO);
            dup2(read_pipe.write_fd(), STDOUT_FILENO);
            write_pipe.close(); read_pipe.close();
            int result;
            if (with_path) {
                if (envp != 0) result = execvpe(argv[0], const_cast<char* const*>(argv), const_cast<char* const*>(envp));
                else result = execvp(argv[0], const_cast<char* const*>(argv));
            }
            else {
                if (envp != 0) result = execve(argv[0], const_cast<char* const*>(argv), const_cast<char* const*>(envp));
                else result = execv(argv[0], const_cast<char* const*>(argv));
            }
            if (result == -1) {
               // Note: no point writing to stdout here, it has been redirected
               std::cerr << "Error: Failed to launch program" << std::endl;
               exit(1);
            }
        }
        else {
            close(write_pipe.read_fd());
            close(read_pipe.write_fd());
            write_buf = std::unique_ptr<__gnu_cxx::stdio_filebuf<char> >(new __gnu_cxx::stdio_filebuf<char>(write_pipe.write_fd(), std::ios::out));
            read_buf = std::unique_ptr<__gnu_cxx::stdio_filebuf<char> >(new __gnu_cxx::stdio_filebuf<char>(read_pipe.read_fd(), std::ios::in));
            stdin.rdbuf(write_buf.get());
            stdout.rdbuf(read_buf.get());
        }
    }
    
    void send_eof() { write_buf->close(); }
    
    int wait() {
        int status;
        waitpid(child_pid, &status, 0);
        return status;
    }
};

// ---------------- Usage example -------------------- //
#include <string>
using std::string;
using std::getline;
using std::cout;
using std::cin;
using std::endl;

int main() {
//    const char* const argv[] = {"/bin/cat", (const char*)0};
    const char* const argv[] = {"./client", (const char*)0};
    char buf[256];
    string s;
    spawn cat(argv);
    while(1){
      cout<< "Input:";
      cin.getline(buf, 256);
      cat.stdin << buf << std::endl;
      getline(cat.stdout, s);
      cout << "Read from child: '" << s << "'" << endl;
      if(strcmp(buf, "exit") == 0) break;
    }
    cout << "Waiting to terminate..." << endl;
    cout << "Status: " << cat.wait() << endl;
    return 0;
}

<popen_test.cpp>

Now let's compile and test the code above.

root@ubuntusrv:/usr/local/src/study/popen# g++ popen_test.cpp -o popen
root@ubuntusrv:/usr/local/src/study/popen# ./popen
Input:Hello World
Read from child: 'from parent:Hello World'
Input:exit
Read from child: 'from parent:exit'
Waiting to terminate...
Status: 0

As in the previous Python example, you can see that two-way communication with the child process works well.


Asynchronous two-way communication

popen_test.cpp works synchronously. In other words, if there is no user input or there is no stdout value of the child process, it is blocked. In other words, you cannot do other work. Blocking can be avoided by using techniques such as multi-threading and multi-processing. However, it is much more natural to handle stdin and stdout asynchronously. All I/O in Linux can be handled as files. File input/output can be processed synchronously or asynchronously, and various methods such as poll, epoll, and select can be used for asynchronous processing. Among them, select shows a significant performance degradation when the number of files monitoring I/O is large. However, in the communication between the parent process and the child process that we currently implement, the number of I/Os to be monitored is 2 to 4, so there is no problem in performance even when using select. 

The following is a slightly modified version of the code above to change the asynchronous method. Timeout is processed when there is no user input for a certain period of time in the parent process's stdin. And if there is no output in the stdout of the child process for a certain period of time, the timeout is processed. This asynchronous processing gives the program considerable flexibility and allows exception handling for input/output timeouts.


// 
// Example of communication with a subprocess via stdin/stdout
// Author: Konstantin Tretyakov
// License: MIT
//

#include <ext/stdio_filebuf.h> // NB: Specific to libstdc++
#include <sys/wait.h>
#include <unistd.h>
#include <iostream>
#include <memory>
#include <exception>
#include <fcntl.h>
#include <sys/select.h>
#include <iosfwd>
#include <cstring>
// Wrapping pipe in a class makes sure they are closed when we leave scope
class cpipe {
private:
    int fd[2];
public:
    const inline int read_fd() const { return fd[0]; }
    const inline int write_fd() const { return fd[1]; }
    cpipe() { if (pipe(fd)) throw std::runtime_error("Failed to create pipe"); }
    void close() { ::close(fd[0]); ::close(fd[1]); }
    ~cpipe() { close(); }
};


//
// Usage:
//   spawn s(argv)
//   s.stdin << ...
//   s.stdout >> ...
//   s.send_eol()
//   s.wait()
//
class spawn {
public:
    cpipe write_pipe;
    cpipe read_pipe;
public:
    int child_pid = -1;
    std::unique_ptr<__gnu_cxx::stdio_filebuf<char> > write_buf = NULL; 
    std::unique_ptr<__gnu_cxx::stdio_filebuf<char> > read_buf = NULL;
    std::ostream stdin;
    std::istream stdout;
    
    spawn(const char* const argv[], bool with_path = false, const char* const envp[] = 0): stdin(NULL), stdout(NULL) {
        child_pid = fork();
        if (child_pid == -1) throw std::runtime_error("Failed to start child process"); 
        if (child_pid == 0) {   // In child process
            dup2(write_pipe.read_fd(), STDIN_FILENO);
            dup2(read_pipe.write_fd(), STDOUT_FILENO);
            write_pipe.close(); read_pipe.close();
            int result;
            if (with_path) {
                if (envp != 0) result = execvpe(argv[0], const_cast<char* const*>(argv), const_cast<char* const*>(envp));
                else result = execvp(argv[0], const_cast<char* const*>(argv));
            }
            else {
                if (envp != 0) result = execve(argv[0], const_cast<char* const*>(argv), const_cast<char* const*>(envp));
                else result = execv(argv[0], const_cast<char* const*>(argv));
            }
            if (result == -1) {
               // Note: no point writing to stdout here, it has been redirected
               std::cerr << "Error: Failed to launch program" << std::endl;
               exit(1);
            }
        }
        else {
            close(write_pipe.read_fd());
            close(read_pipe.write_fd());
            write_buf = std::unique_ptr<__gnu_cxx::stdio_filebuf<char> >(new __gnu_cxx::stdio_filebuf<char>(write_pipe.write_fd(), std::ios::out));
            read_buf = std::unique_ptr<__gnu_cxx::stdio_filebuf<char> >(new __gnu_cxx::stdio_filebuf<char>(read_pipe.read_fd(), std::ios::in));
            stdin.rdbuf(write_buf.get());
            stdout.rdbuf(read_buf.get());
        }
    }
    
    void send_eof() { write_buf->close(); }
    
    int wait() {
        int status;
        waitpid(child_pid, &status, 0);
        return status;
    }
};

// ---------------- Usage example -------------------- //
#include <string>
using std::string;
using std::getline;
using std::cout;
using std::cin;
using std::endl;
static constexpr int STD_INPUT = 0;
static constexpr __suseconds_t WAIT_BETWEEN_SELECT_US = 2500000L;

int main() {
//    const char* const argv[] = {"/bin/cat", (const char*)0};
    const char* const argv[] = {"./client", (const char*)0};
    char buf[256];
    string s;
    spawn cat(argv);
    while(1){
      struct timeval tv = { 0L, WAIT_BETWEEN_SELECT_US };
      buf[0] = 0x00;
      fd_set fds;
      FD_ZERO(&fds);
      FD_SET(STD_INPUT, &fds);
      //cout<< "Input:";
      int ready = select(STD_INPUT + 1, &fds, NULL, NULL, &tv);
      if (ready > 0)
      {
        cin.getline(buf, 256);
        
        int fno = cat.read_pipe.read_fd();
        cat.stdin << buf << std::endl;
        fd_set c_fds;
        FD_ZERO(&c_fds);
        FD_SET(fno, &c_fds);
        int ready = select(fno + 1, &c_fds, NULL, NULL, &tv);
        if (ready > 0){
          getline(cat.stdout, s);
          cout << "Read from child: '" << s << "'" << endl;
          if(strcmp(buf, "exit") == 0) break;
        }
        else{
          cout <<"Child stdout timeout" <<endl;
        }
      }
      else{
        cout <<"Parent stdin timeout" <<endl;
      }
      
    }
    cout << "Waiting to terminate..." << endl;
    cout << "Status: " << cat.wait() << endl;
    return 0;
}

<async_popen_test.cpp>

If you run it after compiling, you can see that the timeout of stdin of the parent process and stdout of the child process is fine as follows.

root@DietPi:/usr/local/src/study# ./async_popen_test
timeout
Helltimeout
o World
Read from child: 'from parent:Hello World'
timeout
Hi
Read from child: 'from parent:Hi'
timeout
exit
Read from child: 'from parent:exit'
Waiting to terminate...
Status: 0


Wrapping up

If you need two-way communication with the child process in C++, you can easily implement the function by using Konstantin Tretyakov's code.


댓글

이 블로그의 인기 게시물

MQTT - C/C++ Client

RabbitMQ - C++ Client #1 : Installing C/C++ Libraries

C/C++ - Everything about time, date