Though this is much easier to do if we use UNIX sockets : )
Let’s cut to the chase and go straight to the code (like we always do :^)
Header file popen2.h:
#ifndef POPEN2_H #define POPEN2_H #include#include // We might also want to use these in C, or provide an interface // for high-level languages like python (though pybind may seem // to be a better option, or is it? :). #ifdef __cplusplus extern "C" { #endif // kinda miss default values struct subprocess_t { pid_t cpid; int exit_code; FILE* p_stdin; // child process's stdin, writable FILE* p_stdout; // child process's stdout, readable }; // though I think calling it `popen1` will be more appropriate :) struct subprocess_t popen2(const char* program); int pclose2(subprocess_t* p); #ifdef __cplusplus } #endif #endif // POPEN2_H
Implementation file popen2.cc:
#include "popen2.h" #include#include #include
#include #include void init_subprocess(subprocess_t* p) { p->cpid = -1; p->exit_code = 0; p->p_stdin = nullptr; p->p_stdout = nullptr; } // thread unsafe :) static std::list opened_processes; subprocess_t popen2(const char* program) { subprocess_t subprocess; // although already initialized (in C++), but maybe not what we want init_subprocess(&subprocess); // We will need 2 pipes in order to communicate between parent and child. // If only using 1 pipe, both child and parent will hold two ends of the // pipe, and now if the parent is going to write to the pipe, data may // get read from parent immediately, without going to the child. int pipe_pc[2]; // write: P -> C / read: C <- P int pipe_cp[2]; // write: C -> P / read: P <- C if (pipe(pipe_pc) < 0) { return subprocess; } if (pipe(pipe_cp) < 0) { close(pipe_pc[0]); close(pipe_pc[1]); return subprocess; } pid_t pid = fork(); if (pid == -1) { // failed close(pipe_pc[0]); close(pipe_pc[1]); close(pipe_cp[0]); close(pipe_cp[1]); return subprocess; } else if (pid == 0) { // child // close unrelated files inherited from parent for (const auto& p : opened_processes) { close(fileno(p.p_stdin)); close(fileno(p.p_stdout)); } close(pipe_cp[0]); // close unused read end in pipe C -> P close(pipe_pc[1]); // close unused write end in pipe P -> C // redirect stdout to write end of the pipe if (pipe_cp[1] != STDOUT_FILENO) { dup2(pipe_cp[1], STDOUT_FILENO); close(pipe_cp[1]); } // redirect stdin to read end of the pipe if (pipe_pc[0] != STDIN_FILENO) { dup2(pipe_pc[0], STDIN_FILENO); close(pipe_pc[0]); } // launch the shell to run the program const char* argp[] = {"sh", "-c", program, nullptr}; execve("/bin/sh", (char**) argp, environ); // shouldn't be reached here _exit(127); } close(pipe_pc[0]); // close unused read end in pipe P -> C close(pipe_cp[1]); // close unused write end in pipe C -> P // parent (client) reads from the subprocess's stdout and // writes to the subprocess's stdin subprocess.p_stdout = fdopen(pipe_cp[0], "r"); subprocess.p_stdin = fdopen(pipe_pc[1], "w"); subprocess.cpid = pid; opened_processes.push_back(subprocess); return subprocess; } int pclose2(subprocess_t* p) { auto it = std::find_if(opened_processes.begin(), opened_processes.end(), [&](const subprocess_t& process) { return process.cpid == p->cpid; }); if (it == opened_processes.end()) { return -1; } fclose(p->p_stdin); fclose(p->p_stdout); pid_t pid = p->cpid; int pstat; do { pid = waitpid(pid, &pstat, 0); } while (pid == -1 && errno == EINTR); // obtain the subprocess's exit code if (WIFEXITED(pstat)) { p->exit_code = WEXITSTATUS(pstat); } else if (WIFSIGNALED(pstat)) { p->exit_code = WTERMSIG(pstat); } opened_processes.erase(it); return (pid == -1 ? -1 : pstat); }
Test client:
#include "popen2.h" #includevoid write_data(subprocess_t& p, const char* data) { // write data to p's stdin fprintf(p.p_stdin, "%s", data); fflush(p.p_stdin); // IMPORTANT, to make sure child receives the data } void read_data(const subprocess_t& p, char* data) { // read data from p's stdout fscanf(p.p_stdout, "%s", data); //fflush(p.p_stdout); } int main() { char buf[5][10]{}; subprocess_t subprocess = popen2("cat"); assert(subprocess.cpid != -1); // communicate with the subprocess // for the test subprocess `cat`, we need a 'n' in each write write_data(subprocess, "onen"); read_data(subprocess, buf[0]); write_data(subprocess, "twon"); write_data(subprocess, "threen"); read_data(subprocess, buf[1]); read_data(subprocess, buf[2]); write_data(subprocess, "fourn"); read_data(subprocess, buf[3]); write_data(subprocess, "fiven"); read_data(subprocess, buf[4]); pclose2(&subprocess); for (char * s : buf) fprintf(stdout, "%sn", s); return 0; }
Final thought:
In popen2.cc, we can incorperate pclose2() to the destructor of
subprocess_t in C++ (given we need to copy the subprocess returned
from popen2(), we may need to add a shared pointer).
class PopenedSubprocess {
shared_ptr p {}; // shared pointer to the process
public:
PopenedSubprocess(const char* program) {
p = make_shared(popen2(program));
}
~PopenedSubprocess() {
if (p.use_count() == 1) // the last one
pclose2(p.get());
}
};
I just made it a repo so that you can run the tests directly. : )



