6

I am trying to pass data from my perl script to my c program using a pipe (uni-directional). I need to find a way to to do this without messing with the child programs STDIN or STDOUT, so I try creating a new handle and passing the fd.

I create 2 IO::Handles and create a pipe. I write to one end of the pipe and attempt to pass the File descriptor of the other end of the pipe to my child program that is being execed. I pass the file descriptor by setting an ENV variable. Why does this not work? (It does not print out 'hello world'). As far as I know, file descriptors and pipes are inherited by the child when exec'd.

Perl script:

#!/opt/local/bin/perl
use IO::Pipe;
use IO::Handle;

my $reader = IO::Handle->new();
my $writer = IO::Handle->new();
$reader->autoflush(1);
$writer->autoflush(1);
my $pipe = IO::Pipe->new($reader, $writer);
print $writer "hello world";
my $fh = $reader->fileno;
$ENV{'MY_FD'} = $fh;
exec('./child') or print "error opening app\n";
# No more code after this since exec replaces the current process

C Program, app.c (Compiled with gcc app.c -o child):

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

int main(int argc, char ** argv) {
  int fd = atoi(getenv("MY_FD"));
  char buf[12];
  read(fd, buf, 11);
  buf[11] = '\0';
  printf("fd: %d\n", fd);
  printf("message: %s\n", buf);
}

Output:

fd: 3
message:

The message is never passed through the pipe to the C program. Any suggestions?

4 Answers4

2

I found the solution. Some people said that it was not possible with exec, that it would not see pipes or file descriptors, but that was not correct.

Turns out that perl closes/invalidates all fd > 2 automatically unless you say otherwise.

Adding the following flags to the FD fixes this problem (where READ is the handle here, NOT STDIN):

my $flags = fcntl(READ, F_GETFD, 0);
fcntl(READ, F_SETFD, $flags & ~FD_CLOEXEC);
2

Your pipe file descriptors are set FD_CLOEXEC, and so are closed upon exec().

Perl's $^F controls this behavior. Try something like this, before you call IO::Pipe->new:

$^F = 10;  # Assumes we don't already have a zillion FDs open

Alternatively, you can with Fcntl clear the FD_CLOEXEC flag yourself after creating the pipe.

pilcrow
  • 56,591
  • 13
  • 94
  • 135
0

Your program is failing because exec calls another program and never returns. It isn't designed for communication with another process at all.

You probably wrote the above code based on the IO::Pipe documentation, which says "ARGS are passed to exec". That isn't what it means, though. IO::Pipe is for communication between two processes within your Perl script, which are created by fork. They mean the execution of the new process, rather than a call to exec in your own code.

Edit: for one-directional communication, all you need is open with a pipe:

open my $prog, '|-', './child' or die "can't run program: $!";

print {$prog} "Hello, world!";
  • 1
    Isn't my case 1-directional? I only need to send data to the exec'd process, not data back to the perl script, this means that I do not need exec to return anything. –  Apr 18 '13 at 09:16
  • 1
    @RodrigoSalazar, oops I thought you were attempting bidirectional communication because your Perl script was both writing to a pipe and reading from a pipe. All you need for one directional communication is `open` with a pipe. –  Apr 18 '13 at 09:21
  • If I use open with a pipe doesn't this mean that data I send to this handle will appear as STDIN to the exec'd process? (I'm not sure) –  Apr 18 '13 at 09:22
  • @RodrigoSalazar, see the updated answer. Yes, it will appear as STDIN. –  Apr 18 '13 at 09:25
  • In my case I can not alter STDIN, it needs to be on a different fd. –  Apr 18 '13 at 09:27
  • @RodrigoSalazar, STDIN is the only way to connect an input pipe that will exist by default in your C program. If you want something else, you have to actually program a way in C to accept the connection. Perhaps you should make a new question that states exactly what task you are trying to accomplish. –  Apr 18 '13 at 09:37
  • I'm pretty sure that's not correct. A forked process (and a replaced process, like exec does) inherits all file descriptors, http://stackoverflow.com/questions/6945865/process-started-from-system-command-in-c-inherits-parent-fds . If I create a file descriptor and write to it, the child should be able to read from it since it inherits it. –  Apr 18 '13 at 09:44
  • 1
    @RodrigoSalazar : Your child process is simply overlaid on top of the main process by using exec. The exec'ed process will have the perl program as its parent, however since its not a forked process, it will not get to see the pipes or file descriptors. – Guru Apr 18 '13 at 10:08
  • Hmm, if I change my exec to system, it should fork. But in that case I still get an invalid FD in my C program. –  Apr 18 '13 at 10:15
0

Rodrigo, I can tell you that your file descriptor is no longer valid when you exec into the c app.
Please be aware that I just say it is INVALID, but it still exists in the environment variables. The FD=3 will continue existing until the whole process ends.
You can check the fd by fcntl. The code is listing below

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

int main(int argc, char ** argv) {
  int fd = atoi(getenv("MY_FD"));
  char buf[12];
  read(fd, buf, 11);
  buf[11] = '\0';
  printf("fd: %d, if fd still valid: %d\n", fd, fcntl(fd, F_GETFD));
  printf("strlen %d\n", (int)strlen(buf));
  printf("message: %s\n", buf);
}

You can see that MY_FD=3 will always in ENV as the process doesn't destroy itself, so you can get fd as 3. But, this file descriptor has been invalid. so the result of fcntl(fd, F_GETFD) will be -1, and the length you read from fd will be 0.
That's why you will never see the "hello world" sentence.

One more thing, @dan1111 is right, but you don't need to open a new pipe, as you have already done so.
All you need to is just set MY_FD=0, like

$ENV{'MY_FD'} = 0;

The STDIN/OUT is another independent process that always exists, so the pipe will not broken down when your perl app exec into c app. That's why you can read from what you input in app.
If your requirement is writing from another file hanle, please try to make that file handle an independent process and always exist, just like STDIN.

noalac
  • 186
  • 3