5

My Question Where can I find a clear working example of C code that sets up and uses a bidirectional pipe with popen() on MacOS?

I am trying to use popen() to open a bidirectional pipe between my C program and an external program on Mac OS X.

My C program has to repeatedly:

  • read an input line from its own stdin
  • reformat this as input for an external program
  • call the external program, passing the formatted input into the external program's stdin
  • read the results from the stdout of the external program
  • process these results and produce some diagnostic output on its own stdout

I can do this easily if I use an actual file to store the input/output and then use system() or similar, but this is too slow, as I have billions of inputs. So I'd like to do it entirely in memory.

The relevant man page, and Internet discussion in this group and others show that I can open a pipe to the external program, then write to it with fprintf just like writing to a file.

pout = popen(external_program, "w")
fprintf(pout,input_to_external_program);

This works fine as a 1-directional pipe, feeding the formatted inputs INTO the external program.

Apparently it is possible on Mac OS to open a bidirectional pipe by specifying "r+" instead of "r" or "w" as the relevant argument to popen, and thereby read and write to the program using the same pipe.

pout = popen(external_program, "r+")
fprintf(pout,"%s", input_to_external_program);
fscanf(fpout,"%s", &output_from_external_program);

I do not need to alternate between reading, writing, reading, writing etc as all that needs to happen is that my program will finish writing the input to the external program before it starts reading the output from the external program.

The external program should just read one self-contained input sequence (i.e. it ends with a special line to indicate "end of input"), and then run to completion, writing output as it goes, then terminating.

But when I try to use this feature, it just doesn't work, because something blocks - the external program starts, but does not complete even one input.

Even when I just forget about doing any reading, but just replace "w" with "r+" then the previously-working functionality stops working.

I have searched for most of this morning for a working popen() example that I can modify, but all that I have found only use a unidirectional pipe.

So we end up back with

My Question *Where can I find a clear working example of C code that sets up and uses a bidirectional pipe with popen() on MacOS?**

If it makes it easier, I might be able to replace the C part with Perl.

UPDATE

Thanks for the answers, but I am still unable to make it work properly though I can make some small-scale versions work. For example, I wrote a little program that would just add one to any numbers fed to it, quitting if the input is -99/ I called this "copy".

#include <stdio.h>
#include <unistd.h>

int 
main(int argc, char **argv) {

  int k;

  setbuf(stdout,NULL);

  while (scanf("%d",&k) == 1) {
    if (k == -99) break;
    printf("%d\n",k+1);
  }

}

Then I wrote a parent program that will create the pipe, write some stuff to it, and then wait for the output (not doing NOHANG because I want the parent to keep sucking up the output of the child until the child is done).

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(void) {

    FILE *p;
    int  k;

    p = popen("copy", "r+");
    if (!p) {
        puts("Can't connect to copy");
        return 1;
    }

    fprintf(p,"%d\n",100);
    fprintf(p,"%d\n",200);
    fprintf(p,"%d\n",300);
    fprintf(p,"%d\n",-99);

    fflush(p);

    while (fscanf(p,"%d",&k) == 1)  {
      printf("%d\n", k);
    }

    pclose(p);

    return 0;
 }

This works perfectly, but as soon as I replace the dummy program "copy" with my actual subprocess, it just hangs.

Everything should be the same - the parent process creates an entire input sequence for the child, which reads it, does some work, outputs some arbitrary number of lines of output and then terminates.

But something is different - either the child process is not receiving all of its input, or is buffering its output or something. Mysterious.

Gordon Royle
  • 256
  • 1
  • 11
  • Perhaps check this, it might give some hints about bi-directional pipe: http://stackoverflow.com/questions/8390799/can-pipes-in-unix-work-bi-directionally – danglingpointer May 18 '17 at 08:37
  • an excerpt from the man page for `popen()`. "The popen() function opens a process by creating a pipe, forking, and invoking the shell. Since a pipe is by definition unidirectional, the type argument may specify only reading or writing, not both; the resulting stream is correspondingly read-only or write-only." – user3629249 May 18 '17 at 16:38
  • if you want to have two way communication, rather than `popen()`, use `fork()` and inside the child process, before 'exec'ing the 'bc process, modify `stdin` and `stdout` – user3629249 May 18 '17 at 17:10
  • in the posted code, where does `fpout` come from? – user3629249 May 18 '17 at 17:12
  • 1
    @user3629249: That's not true on macOS. An excerpt from relevant [man page](http://www.manpages.info/macosx/popen.3.html): *"Since popen() is now implemented using a bidirectional pipe, the type argument may request a bidirectional data flow"* – el.pescado - нет войне May 19 '17 at 08:04
  • Does your "actual process" also `setbuf(stdout,NULL);` like your dummy "copy" program? – kmkaplan May 19 '17 at 12:01
  • @kmkaplan Actually the setbuf makes no difference - the dummy program works with or without it, and when I added it to the real program (I have the source code although I did not write it) it still didn't work with or without it. However due to help from the person who *did* write it, I have now implemented the "two-pipe" solution with pipe() / fork() / execvp() and this is working. – Gordon Royle May 20 '17 at 02:17
  • Given your POC with your `copy` program, if it doesn't work with your other program, the problem is presumably with that other program. What can you say about the other program. Does it require a terminal for input? Does it work sanely in the middle of a pipeline such as `printf '%d\n' 100 200 300 -99 | mystery-program | cat`? If it doesn't work here, it won't work with the bidirectional pipe either. – Jonathan Leffler Dec 24 '17 at 17:03
  • Normally (according to standard C), on a regular file stream opened for `"r+"`, you must `fseek()` when switching between reading and writing (or vice versa). The man page is silent on whether this circumlocution is necessary for a bidirectional `popen()`. It probably isn't. – Jonathan Leffler Dec 24 '17 at 17:04
  • Does this still work with screen ? It's giving me a "must be connected to a terminal" message – StarckOverflar Nov 19 '18 at 17:14

2 Answers2

4

Here's a program that interacts with bc to perform integer calculations. In the simplest case, all you need to do is use fgets() and fputs() to read and write to/from the pipe.

However, if you enter something that produces no output, like setting a variable (e.g., x=1) or a runtime error (e.g., 1/0), then fgets() will continue waiting for input indefinitely. To avoid this situation, you have to make the pipe non-blocking and check it repeatedly for output from the piped process.

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main(void) {
    FILE *p;
    char buf[1025];
    int i, fd, delay;

    p = popen("bc", "r+");
    if (!p) {
        puts("Can't connect to bc");
        return 1;
    }
    /* Make the pipe non-blocking so we don't freeze if bc produces no output */
    fd = fileno(p);
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);

    puts("Give me a sum to do (e.g., 2+2):");
    if (fgets(buf, 1024, stdin)) {
        fputs(buf, p);
        fflush(p);
        for (delay=1,i=0; i<20; delay*=2,i++) {
            usleep(delay);
            if (fgets(buf, 1024, p)) {
                printf("The answer is %s", buf);
                break;
            }
        }
    }
    pclose(p);

    return 0;
}

Also, don't forget that pipes are generally line buffered. This isn't a problem here because fgets() includes a newline character at the end of the input. Correction: pipes are block buffered, so call fflush() after sending data into the pipe.

Community
  • 1
  • 1
r3mainer
  • 23,981
  • 3
  • 51
  • 88
  • the posted answer (almost) compiles. However, when actually run the call to `popen()` fails. If the code were to use `perror()` instead of `puts()` so that reason the OS thinks the call to `popen()` is also output, this is the result: `can't connect to bc: Invalid argument` ` – user3629249 May 18 '17 at 16:46
  • here is an excerpt from the man page for `popen`, regarding the second parameter: "The type argument is a pointer to a null-terminated string which must contain either the letter 'r' for reading or the letter 'w' for writ‐ ing." – user3629249 May 18 '17 at 16:55
  • I say almost compiles because the compiler will output: warning: conversion to '__useconds_t (aka unsigned int)' from 'int' may change the sign of the result [-Wsign-conversion] due to the parameter to `usleep()` – user3629249 May 18 '17 at 16:58
  • BTW: error messages should be output to `stderr`, not `stdout` – user3629249 May 18 '17 at 16:59
  • 1
    when a pipe has been opened with `popen()` it should not be closed with `fclose()` but rather with: `pclose()` – user3629249 May 18 '17 at 17:00
  • 1
    BTW: pipes are 'block buffered', not 'line buffered' – user3629249 May 18 '17 at 17:03
  • caveat: I'm using ubuntu linux, not MAX OS – user3629249 May 18 '17 at 17:15
  • 1
    note: `pipe()` is a well known C library function. It is a poor programming practice to use the same name as a function as a variable name. – user3629249 May 18 '17 at 17:18
  • 1
    @user3629249 Thanks for taking a look at this. FYI, bidirectional pipes *are* supported in OS X. Quote: *"The mode argument is a pointer to a null-terminated string which must be 'r' for reading, 'w' for writing, or 'r+' for reading and writing."* The code compiles and runs without any errors, even without calling `fflush()` after printing to the pipe. Yes, I know error messages belong on stderr. It's also possible that bc might return more than one line of output, but can't see much point in covering every eventuality given that this is just a proof of concept. – r3mainer May 18 '17 at 19:10
1

Make sure that you fflush(pout) in the parent process and that the child process does fflush(stdout) after every output.

kmkaplan
  • 18,655
  • 4
  • 51
  • 65