9

I've written a simple C++ program for tutorial purposes. My goal is to loop it infinitely.

#include <iostream>
#include <string>

int main()
{
  std::cout << "text";

  for(;;) {
    std::string string_object{};
    std::getline(std::cin, string_object);
    std::cout << string_object;
  }

  return 0;
}

After compilation I run it like this: ./bin 0>&1 What I expected to happen is that the "text" that is output to stdout, and it will now become also stdin for the program and it will loop forever. Why doesn't it happen?

oguz ismail
  • 1
  • 16
  • 47
  • 69
ziemowit141
  • 465
  • 3
  • 18
  • 3
    That's not how I/O streams work. Writing to the terminal doesn't put the data into the keyboard input. – Barmar Sep 30 '20 at 23:06
  • 1
    You can use a `std::stringstream` to do such things. – πάντα ῥεῖ Sep 30 '20 at 23:07
  • 2
    Exactly why do you want to do this? Sounds fun to achieve and I like the question, but I'm curious if there is an actual use case for this. – klutt Sep 30 '20 at 23:09
  • 1
    @Barmar could you please make the distinction for me? Isn't it accomplished by redirecting 0>&1? – ziemowit141 Sep 30 '20 at 23:09
  • @klutt just to better understand I/O operations, sorry xD – ziemowit141 Sep 30 '20 at 23:10
  • 1
    FD1 is connected to `/dev/tty`. So `0>&1` is the same as `0>/dev/tty`. So it redirects stdin to the terminal, which reads from the keyboard. – Barmar Sep 30 '20 at 23:10
  • 2
    But stdin is an input stream, you can't use `>` to redirect it, you should use `<`. But it's the same problem. – Barmar Sep 30 '20 at 23:11
  • 1
    @ziemowit141 I think this can help you. Might even be a duplicate for this. https://stackoverflow.com/questions/7383803/writing-to-stdin-and-reading-from-stdout-unix-linux-c-programming Tell me if it is what you're looking for – klutt Sep 30 '20 at 23:16
  • 1
    @ziemowit141 Related: https://stackoverflow.com/questions/63967830/capture-a-functions-standard-output-and-write-it-to-a-file/63968064#63968064 – πάντα ῥεῖ Sep 30 '20 at 23:21
  • Hey thank you guys! Someone can close as a duplicate? – ziemowit141 Sep 30 '20 at 23:22
  • @ziemowit141 _"Someone can close as a duplicate?"_ I could. Which one of the links you want to use as the duplicate? Along with my answer, and if you don't insist to do that via external redirections from the shell, you could use a `std::stringstream::rdbuf()` to redirect both `std::cout` and `std::cin`. – πάντα ῥεῖ Sep 30 '20 at 23:34
  • @ziemowit: it's not duplicate; in this case OP wants to see a nice loop I guess. – kisch Sep 30 '20 at 23:37
  • @ πάντα ῥεῖ: you can use a stringstream to loop inside of the program, but the OP specifically wants an endless loop through the external file descriptors. – kisch Sep 30 '20 at 23:40
  • @kisch _"you can use a stringstream to loop inside of the program"_ That won't catch outputs / inputs made from other functions which call `std::cout` or `std::cin` directly. – πάντα ῥεῖ Sep 30 '20 at 23:42
  • @ πάντα ῥεῖ: maybe not. but that wasn't asked for, was it? "My goal is to loop it infinitely." Which my solution does. – kisch Sep 30 '20 at 23:45
  • @kisch It would be possible to loop infinitely with my solution as well, I don't see your point. – πάντα ῥεῖ Oct 01 '20 at 00:03
  • @ πάντα ῥεῖ: OP specifically tried to loop through stdout connected back to stdin externally - that's how I read the question. You solution loops internally. – kisch Oct 01 '20 at 00:12

3 Answers3

2

First, you need to output newlines when printing to std::cout, otherwise std::getline() won't have any complete line to read.

Improved version:

#include <iostream>
#include <string>

int main()
{
  std::cout << "stars" << std::endl;

  for(;;) {
    std::string string_object;
    std::getline(std::cin, string_object);
    std::cout << string_object << std::endl;
  }

  return 0;
}

Now try this:

./bin >file <file

you don't see any output, because it's going to the file. But if you stop the program and look at the file, behold, it's full of

stars
stars
stars
stars

:-)

Also, the reason that the feedback loop cannot start when you try

./bin 0>&1

is, that you end up with both stdin and stdout connected to /dev/tty (meaning that you can see the output).

But a TTY device cannot ever close the loop, because it actually consists of two separate channels, one passing the output to the terminal, one passing the terminal input to the process.

If you use a regular file for in- and output, the loop can be closed. Every byte written to the file will be read from it as well, if the stdin of the process is connected to it. That's as long as no other process reads from the file simultaneously, because each byte in a stream can be only read once.

kisch
  • 414
  • 2
  • 6
  • I fixed my explanation why it's necessary to output std::endl (so getline() sees terminated lines coming in; nothing to do with line-buffering) – kisch Sep 30 '20 at 23:56
  • Deleted my first comment, put it in the answer instead, to answer the question more comprehensively. – kisch Oct 01 '20 at 00:35
  • fixed explanation concerning special behaviour of TTY deivces (thanks to @user414777) – kisch Oct 01 '20 at 03:38
  • Thank you for so much effort. Your example is very precise, and this was the key information for me to fully comprehend the question: "But a TTY device cannot ever close the loop, because it actually consists of two separate channels, one passing the output to the terminal, one passing the terminal input to the process" – ziemowit141 Oct 01 '20 at 09:33
0

Because the stdout and stdin don't create a loop. They may point to the same tty, but a tty is actually two separate channels, one for input and one for output, and they don't loop back into one another.

You can try creating a loop by running your program with its stdin connected to the read end of a pipe, and with its stdout to its write end. That will work with cat:

mkfifo fifo
{ echo text; strace cat; } <>fifo >fifo
...
read(0, "text\n", 131072)               = 5
write(1, "text\n", 5)                   = 5
read(0, "text\n", 131072)               = 5
write(1, "text\n", 5)                   = 5
...

But not with your program. That's because your program is trying to read lines, but its writes are not terminated by a newline. Fixing that and also printing the read line to stderr (so we don't have to use strace to demonstrate that anything happens in your program), we get:

#include <iostream>
#include <string>

int main()
{
  std::cout << "text" << std::endl;

  for(;;) {
    std::string string_object{};
    std::getline(std::cin, string_object);
    std::cerr << string_object << std::endl;
    std::cout << string_object << std::endl;
  }
}
g++ foo.cc -o foo
mkfifo fifo; ./foo <>fifo >fifo
text
text
text
...

Note: the <>fifo way of opening a named pipe (fifo) was used in order to open both its read and its write end at once and so avoid blocking. Instead of reopening the fifo from its path, the stdout could simply be dup'ed from the stdin (prog <>fifo >&0) or the fifo could be first opened as a different file descriptor, and then the stdin and stdout could be opened without blocking, the first in read-only mode and the second in write-only mode (prog 3<>fifo <fifo >fifo 3>&-).

They will all work the same with the example at hand. On Linux, :|prog >/dev/fd/0 (and echo text | strace cat >/dev/fd/0) would also work -- without having to create a named pipe with mkfifo.

  • You don't actually need a FIFO to close the loop. That just complicates the picture. A simple file is completely sufficient. I demonstrate this in my answer. You're right that a TTY would never be able to close the loop; I should correct my answer in that respect. – kisch Oct 01 '20 at 03:26
  • IMHO, it's using a regular file which complicates the picture (besides filling up the disk ;-)). It involves hairy issues of file pointer positions and non-synchronized reads and writes to the same file. –  Oct 01 '20 at 03:37
  • Yeah, but a FIFO adds an intermediate instance between input and output. Input and output are not *directly* connected. This is was OP wanted to achieve. With a regular file, you have the file contents as a kind of unnecessary side effect. The question asks how to directly feed back the process output to its input; and a regular file is as close it gets (or as I can think of). – kisch Oct 01 '20 at 03:50
  • @kisch You're confused -- since you're opening the file twice, you're actually using *two* files refering to the same on-disk inode, and **it's a fluke** that the reads and writes are ordered as expected. (And you also have like a dozen layers between the two open file objects, but that's beside the point). And of course, your example will not work as expected if you're using a single file, opened in read-write mode, with the same file pointer, something which you can emulate with `./bin 1<>file 0<&1`. –  Oct 02 '20 at 03:12
0

Since you're using gcc, I'm going to assume you have pipe available.

#include <cstring>
#include <iostream>
#include <unistd.h>

int main() {
    char buffer[1024];
    std::strcpy(buffer, "test");

    int fd[2];
    ::pipe(fd);
    ::dup2(fd[1], STDOUT_FILENO);
    ::close(fd[1]);
    ::dup2(fd[0], STDIN_FILENO);
    ::close(fd[0]);

    ::write(STDOUT_FILENO, buffer, 4);
    while(true) {
        auto const read_bytes = ::read(STDIN_FILENO, buffer, 1024);
        ::write(STDOUT_FILENO, buffer, read_bytes);
#if 0
        std::cerr.write(buffer, read_bytes);
        std::cerr << "\n\tGot " << read_bytes << " bytes" << std::endl;
#endif
        sleep(2);
    }
    return 0;
}

The #if 0 section can be enabled to get debugging. I couldn't get it to work with std::cout and std::cin directly, but somebody who knows more about the low-level stream code could probably tweak this.

Debug output:

$ ./io_loop 
test
        Got 4 bytes
test
        Got 4 bytes
test
        Got 4 bytes
test
        Got 4 bytes
^C
Stephen Newell
  • 7,330
  • 1
  • 24
  • 28