2

I'm working on a data pipeline node, an application that takes a continuous supply of data from stdin, processes it, and outputs the result continuously to stdout in a streaming fashion.

Considering the data interchange format is pre-determined, I need a handy way of barring my debug output from feeding to stdout simultaneously. Essentially, a global lock. True, I could just get rid of all the debug statements, but it's more of an academic exercise.

So let's make a function which can write to stdout, and locks stdout as long as it remains in scope so the type system itself prevents other places in the code from writing to stdout:

use std::io::{self, Write};

pub fn make_push_output<'a>() -> &'a impl Fn(String) -> io::Result<()> {
    let handle = io::stdout().lock();

    &|output: String| {
        handle.write(output.to_string().as_bytes())?;

        Ok(())
    }
}

Cool, a global lock on stdout that stays in place until the output push_output() function goes out of scope, but it doesn't work. I get a whole list of borrow checker errors:

error[E0597]: borrowed value does not live long enough
 --> src/lib.rs:4:18
  |
4 |     let handle = io::stdout().lock();
  |                  ^^^^^^^^^^^^       - temporary value only lives until here
  |                  |
  |                  temporary value does not live long enough
  |
  = note: borrowed value must be valid for the static lifetime...

error[E0597]: borrowed value does not live long enough
  --> src/lib.rs:6:6
   |
6  |       &|output: String| {
   |  ______^
7  | |         handle.write(output.to_string().as_bytes())?;
8  | |
9  | |         Ok(())
10 | |     }
   | |_____^ temporary value does not live long enough
11 |   }
   |   - temporary value only lives until here
   |
note: borrowed value must be valid for the lifetime 'a as defined on the function body at 3:25...
  --> src/lib.rs:3:25
   |
3  | pub fn make_push_output<'a>() -> &'a impl Fn(String) -> io::Result<()> {
   |                         ^^

error[E0373]: closure may outlive the current function, but it borrows `handle`, which is owned by the current function
 --> src/lib.rs:6:6
  |
6 |     &|output: String| {
  |      ^^^^^^^^^^^^^^^^ may outlive borrowed value `handle`
7 |         handle.write(output.to_string().as_bytes())?;
  |         ------ `handle` is borrowed here
help: to force the closure to take ownership of `handle` (and any other referenced variables), use the `move` keyword
  |
6 |     &move |output: String| {
  |      ^^^^^^^^^^^^^^^^^^^^^

error[E0387]: cannot borrow data mutably in a captured outer variable in an `Fn` closure
 --> src/lib.rs:7:9
  |
7 |         handle.write(output.to_string().as_bytes())?;
  |         ^^^^^^
  |
help: consider changing this closure to take self by mutable reference
 --> src/lib.rs:6:6
  |
6 |       &|output: String| {
  |  ______^
7 | |         handle.write(output.to_string().as_bytes())?;
8 | |
9 | |         Ok(())
10| |     }
  | |_____^

I've been trying for well over an hour to fix this sequence of borrow checker errors on these 7 lines of code. Here's a non-exhaustive list of steps I've taken so far that haven't worked:

  • Change the lifetime of make_push_output
  • Add explicit type and lifetime annotation to handle
  • Declare a variable for io::stdout() and annotate with type and lifetime
  • Add explicit type and lifetime annotation to the closure
  • Declare a local function instead of using a closure, couldn't capture the environment
  • Use move semantics on the closure, not the brightest move, but I was grasping at straws
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
TheEnvironmentalist
  • 2,694
  • 2
  • 19
  • 46
  • Regardless of the error you get, locking a handle to stdout doesn't actually buy you anything – it only locks this specific handle. You can easily create and use a different handle, even while the handle you hold is locked. – Sven Marnach Nov 09 '18 at 17:35

1 Answers1

3

You cannot. The standard output lock is reentrant:

use std::io::{self, Write};

fn main() {
    let out = io::stdout();
    let mut handle = out.lock();
    writeln!(handle, "handle: {}", 1);

    println!("Debugging output");

    writeln!(handle, "handle: {}", 2);
    drop(handle)
}

This prints:

handle: 1
Debugging output
handle: 2

No amount of type juggling can prevent the same thread from re-acquiring a lock to standard out / error and printing in the middle of your output.


Your compiler errors are addressed by:

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • So a theoretical option would be to lock stdout in a _different_ thread, and push all desired output to a queue which is then printed to stdout by the thread that holds the lock. However, this would lead to a deadlock as soon as the main thread tries to use `println!()`. – Sven Marnach Nov 10 '18 at 14:38
  • @SvenMarnach and it still wouldn't technically meet OPs requirement to use *the type system* to solve the problem, as AFAICT Rust's type system is simply not powerful enough to provide that restriction. – Shepmaster Nov 10 '18 at 14:43