2

I'm building a PromptSet that can ask a series of questions in a row. For testing reasons, it allows you to pass a reader and writer instead of using stdin & stdout directly.

Because stdin and stdout are the common use case, I would like to create a default "constructor" that allows the user to produce a PromptSet<StdinLock, StdoutLock> without needing any parameters. Here's the code so far:

use std::io::{self, BufRead, StdinLock, StdoutLock, Write};

pub struct PromptSet<R, W>
where
    R: BufRead,
    W: Write,
{
    pub reader: R,
    pub writer: W,
}

impl<R, W> PromptSet<R, W>
where
    R: BufRead,
    W: Write,
{
    pub fn new(reader: R, writer: W) -> PromptSet<R, W> {
        return PromptSet {
            reader: reader,
            writer: writer,
        };
    }

    pub fn default<'a>() -> PromptSet<StdinLock<'a>, StdoutLock<'a>> {
        let stdin = io::stdin();
        let stdout = io::stdout();

        return PromptSet {
            reader: stdin.lock(),
            writer: stdout.lock(),
        };
    }

    pub fn prompt(&mut self, question: &str) -> String {
        let mut input = String::new();

        write!(self.writer, "{}: ", question).unwrap();
        self.writer.flush().unwrap();
        self.reader.read_line(&mut input).unwrap();

        return input.trim().to_string();
    }
}

fn main() {}

StdinLock and StdoutLock both need a lifetime declared. To complicate it, I think the original stdin()/stdout() handles need to live at least as long as the locks do. I would like the references to StdinLock and StdoutLock to live as long as my PromptSet does but no matter what I try I can't get it to work. Here is the error that I keep getting:

error[E0597]: `stdin` does not live long enough
  --> src/main.rs:30:21
   |
30 |             reader: stdin.lock(),
   |                     ^^^^^ borrowed value does not live long enough
...
33 |     }
   |     - borrowed value only lives until here
   |
note: borrowed value must be valid for the lifetime 'a as defined on the method body at 25:5...
  --> src/main.rs:25:5
   |
25 | /     pub fn default<'a>() -> PromptSet<StdinLock<'a>, StdoutLock<'a>> {
26 | |         let stdin = io::stdin();
27 | |         let stdout = io::stdout();
28 | |
...  |
32 | |         };
33 | |     }
   | |_____^

error[E0597]: `stdout` does not live long enough
  --> src/main.rs:31:21
   |
31 |             writer: stdout.lock(),
   |                     ^^^^^^ borrowed value does not live long enough
32 |         };
33 |     }
   |     - borrowed value only lives until here
   |
note: borrowed value must be valid for the lifetime 'a as defined on the method body at 25:5...
  --> src/main.rs:25:5
   |
25 | /     pub fn default<'a>() -> PromptSet<StdinLock<'a>, StdoutLock<'a>> {
26 | |         let stdin = io::stdin();
27 | |         let stdout = io::stdout();
28 | |
...  |
32 | |         };
33 | |     }
   | |_____^

It's perfectly possible I just don't understand the concept of lifetimes or something else super basic.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
webdesserts
  • 1,003
  • 8
  • 22
  • If the question is rephrased to stdin/stdout it's not a duplicate, since stdin/stdout are a rather special case. – oli_obk Jan 09 '17 at 14:10

2 Answers2

8

The lock method's signature is fn lock(&self) -> StdinLock, which, when fully expanded with lifetime annotations, is fn lock<'a>(&'a self) -> StdinLock<'a>. Thus the StdinLock can only live as long as the value that the lock method is called on. Since you defined stdin in this very function, the StdinLock can't outlive the function. This is the same as returning a reference to a local value. You also can't return the reference and the referred-to value together.

You can't do this, and you can't work around it. The only fix is to have the default method take a Stdin and a Stdout object as arguments.

That said, you can work around it. Yes I know, I just said the exact opposite, but it's more of a "no one other than me will ever use stdin/stdout" (a.k.a., println! will not work anymore!).

In Rust 1.26, you can use Box::leak to leak the Stdin to a &'static Stdin, which will yield a StdinLock<'static>. Before Rust 1.26, you can use the leak crate:

pub fn default() -> PromptSet<StdinLock<'static>, StdoutLock<'static>> {
    let stdin = Box::leak(Box::new(io::stdin()));
    let stdout = Box::leak(Box::new(io::stdout()));

    PromptSet {
        reader: stdin.lock(),
        writer: stdout.lock(),
    }
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
oli_obk
  • 28,729
  • 6
  • 82
  • 98
  • Yeah, I've considered the `'static` lifetime but, wow, I didn't think that would cause problems too. I can see the usability issues in taking control of `stdin` & `stdout` for prolonged periods of time so perhaps I'll let this be. Thank you for the explanation. – webdesserts Jan 09 '17 at 16:59
  • Wouldn't it be possible to implement `default` that returns `PrompSet`, with `HelperIn` and `HelperOut` being classes that contain (own) instances of `Stdin` and `Stdout`, and implement `BufRead` and `Write` respectively (by locking the stream for each operation and calling the appropriate method on the lock guard)? It seems like it should be possible, but when I wrote it up, it wouldn't compile due to `BufRead::fill_buf` not satisfying the life-time requirements. – user4815162342 Jan 09 '17 at 22:02
  • 1
    That would create a broken `BufRead` implementation. You would loose all buffered bytes that weren't read within the `BufRead` call. What you can do is to implement the buffering yourself, but then you would be mixing arbitrary bytes between different readers. There's a reason `StdinLock` doesn't already work like this. – oli_obk Jan 10 '17 at 07:09
  • This is extremely useful in case when you e.g. need to lock stdin and pass it to code that (for various technical reasons) expects `impl Read + 'static` with the intent of exhausting it. Since stdin is about to be exhausted and it's a short-lived process, it is not an issue that it will not be usable afterwards. – user4815162342 Oct 02 '20 at 15:34
1

Might be not really the answer to your question, but to a similar problem. Here's my solution.

The main trick here is to call stdin.lock() for every single line.

use std::io;
use std::io::prelude::*;
use std::io::Stdin;

struct StdinWrapper {
    stdin: Stdin,
}

impl Iterator for StdinWrapper {
    type Item = String;

    fn next(&mut self) -> Option<Self::Item> {
        let stdin = &self.stdin;
        let mut lines = stdin.lock().lines();
        match lines.next() {
            Some(result) => Some(result.expect("Cannot read line")),
            None => None,
        }
    }
}

/**
 * Callers of this method should not know concrete source of the strings.
 * It could be Stdin, a file, DB, or even aliens from SETI.
 */
fn read() -> Box<Iterator<Item = String>> {
    let stdin = io::stdin();
    Box::new(StdinWrapper { stdin })
}

fn main() {
    let lines = read();

    for line in lines {
        println!("{}", line);
    }
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Dzmitry Lazerka
  • 1,809
  • 2
  • 21
  • 37