Since you're already working with C, then I'd suggest you manage stockchess in C as well. There is a library function, popen() that will give you a unidirectional pipe to a process -- that doesn't suite your use case. You can, however, set it up yourself.
Consider the following example program:
#include <errno.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> /** * Creates two pipes, forks, and runns the given command. One pipe is * connected between the given *out and the standard input stream of the child; * the other pipe is connected between the given *in and the standard output * stream of the child. * * Returns the pid of the child on success, -1 otherwise. On error, errno * will be set accordingly. */ int bi_popen(const char* const command, FILE** const in, FILE** const out) { const int READ_END = 0; const int WRITE_END = 1; const int INVALID_FD = -1; int to_child[2] = { INVALID_FD, INVALID_FD }; int to_parent[2] = { INVALID_FD, INVALID_FD }; *in = NULL; *out = NULL; if (command == NULL || in == NULL || out == NULL) { errno = EINVAL; goto bail; } if (pipe(to_child) < 0) { goto bail; } if (pipe(to_parent) < 0) { goto bail; } const pid_t pid = fork(); if (pid < 0) { goto bail; } if (pid == 0) { // Child if (dup2(to_child[READ_END], STDIN_FILENO) < 0) { perror("dup2"); exit(1); } close(to_child[READ_END]); close(to_child[WRITE_END]); if (dup2(to_parent[WRITE_END], STDOUT_FILENO) < 0) { perror("dup2"); exit(1); } close(to_parent[READ_END]); close(to_parent[WRITE_END]); execlp(command, command, NULL); perror("execlp"); exit(1); } // Parent close(to_child[READ_END]); to_child[READ_END] = INVALID_FD; close(to_parent[WRITE_END]); to_parent[WRITE_END] = INVALID_FD; *in = fdopen(to_parent[READ_END], "r"); if (*in == NULL) { goto bail; } to_parent[READ_END] = INVALID_FD; *out = fdopen(to_child[WRITE_END], "w"); if (*out == NULL) { goto bail; } to_child[WRITE_END] = INVALID_FD; setvbuf(*out, NULL, _IONBF, BUFSIZ); return pid; bail: ; // Goto label must be a statement, this is an empty statement const int old_errno = errno; if (*in != NULL) { fclose(*in); } if (*out != NULL) { fclose(*out); } for (int i = 0; i < 2; ++i) { if (to_child[i] != INVALID_FD) { close(to_child[i]); } if (to_parent[i] != INVALID_FD) { close(to_parent[i]); } } errno = old_errno; return -1; } int main(void) { FILE* in = NULL; FILE* out = NULL; char* line = NULL; size_t size = 0; const int pid = bi_popen("/bin/bash", &in, &out); if (pid < 0) { perror("bi_popen"); return 1; } fprintf(out, "ls -l a.out\n"); getline(&line, &size, in); printf("-> %s", line); fprintf(out, "pwd\n"); getline(&line, &size, in); printf("-> %s", line); fprintf(out, "date\n"); getline(&line, &size, in); printf("-> %s", line); // Since in this case we can tell the child to terminate, we'll do so // and wait for it to terminate before we close down. fprintf(out, "exit\n"); waitpid(pid, NULL, 0); fclose(in); fclose(out); return 0; }
In the program, I have defined a function bi_popen. The function take as input the path of a program to run as well as two FILE*: in for input from the command and out for output to the command.
bi_popen sets up two pipes, one for communicating from the parent process to the child process, and another for communicating from the child process to the parent process.
Next, bi_popen forks, creating a new process. The child process connects its standard output to the write-end of the pipe to the parent, and connects its standard input to the read-end of the pipe from the parent. It then cleans up the old pipe file descriptors and uses execlp to replace the running process with the given command. That new program inherits the standard input/output configuration with the pipes. On success, execlp never returns.
In the case of the parent --- when fork returns a non-zero value ---, the parent process closes the unnecessary ends of the pipe, and uses fdopen to create FILE* associated with the relevant pipe file descriptors. It updates the in and out output parameters with those values. Finally, it uses execlp on the output FILE* to make it unbuffered (so that you don't have to explicitly flush content you send to the child process).
The main function is an example of how to use the bi_popen function. It calls bi_popen with the command as /bin/bash. Anything written to the out stream is sent to bash to execute. Anything bash prints to standard output is available for reading from in.
Here's an example run of the program:
$ ./a.out -> -rwxr-xr-x 1 user group 20400 Aug 29 17:09 a.out -> /home/user/src/bidirecitonal_popen -> Sat Aug 29 05:10:52 PM EDT 2020
Note that main writes a series of commands to the child process (here, bash), and the command responded with the expected output.
In your case, you could replace "/bin/bash" with "stockfish", then use out to write commands to stockfish and in to read the responses.