1

I'd like to store a reference to an io::Write trait object inside an Option in a struct but I can't figure out how. I can put the reference in directly like this:

pub struct Parameters<'a> {
    pub log: &'a (io::Write + 'a),
    // Other elements removed
}

and then assign it from (for example) a BufWriter like this:

let logstream = &BufWriter::new(f);
let parameters = Parameters {
    log: logstream, // Other elements removed
};

This works, but I'd like the logstream to be optional. If I try:

pub struct Parameters<'a> {
    pub log: Option<&'a(io::Write + 'a)>,
    // Other elements removed
}

and

let logstream = match f {
    Some(f) => Some(&BufWriter::new(f)),
    None => None,
};

let parameters = Parameters {
    log: logstream,
    // Other elements removed
};

I get:

error[E0308]: mismatched types
  --> src/main.rs:17:14
   |
17 |         log: logstream,
   |              ^^^^^^^^^ expected trait std::io::Write, found struct `std::io::BufWriter`
   |
   = note: expected type `std::option::Option<&dyn std::io::Write>`
              found type `std::option::Option<&std::io::BufWriter<std::vec::Vec<u8>>>`

What is a suitable approach here?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Bob
  • 1,037
  • 1
  • 9
  • 14
  • Once you get this past the type checker, the borrow checker will kill you (the `BufWriter` doesn't live long enough). – starblue Apr 20 '19 at 08:41

3 Answers3

0

You have to somehow explicitly type the BufWriter with the trait type, and you also have to be more careful with lifetimes.

Here is a sketch of how this could work:

use std::io;
use std::io::BufWriter;
use std::str::from_utf8;

pub struct Parameters<'a> {
    pub log: Option<&'a mut io::Write>,
    // Other elements removed
}

fn main() {
    let mut w = BufWriter::new(Vec::new());
    let rw: &mut std::io::Write = &mut w;
    let logstream = Some(rw);

    let parameters = Parameters {
        log: logstream,
        // Other elements removed
    };
    parameters.log.unwrap().write(b"hello world").ok();

    println!("{}", from_utf8(&w.into_inner().unwrap()).unwrap());
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
starblue
  • 55,348
  • 14
  • 97
  • 151
  • Thanks, starblue. Actually, I think I may have found another answer using Option> - I'll post it here for comment once I've finished checking it out. – Bob Apr 20 '19 at 09:50
0
use std::io;
use std::io::BufWriter;

pub struct P<'a> {
    pub log: Option<&'a mut io::Write>,
}

fn file() -> Option<Vec<u8>> {
    Some(Vec::new())
}

fn main() {
    let mut ow = file().map(|f| BufWriter::new(f));
    let p = P {
        log: ow.as_mut().map(|w| w as &mut io::Write),
    };

    p.log.unwrap().write(b"Hi!").ok();

    println!(
        "{}",
        String::from_utf8(ow.unwrap().into_inner().unwrap()).unwrap()
    );
}

Rather than taking an Option<W> and try and convert it into an Option<&mut io::Write> in one go, do it in three steps:

  • Option<W> -> Option<io::BufWriter<W>> to make ow. This is the thing that will own the BufWriter. As pointed out in the other answer, if you try and create a BufWriter within the pattern match and then immediately take a reference to it, that reference's lifetime is scoped within the branch of the pattern match, so the borrow checker will complain.
  • The next step is to convert from Option<io::BufWriter<W>> to Option<&mut io::BufWriter<W>> using as_mut().
  • Finally convert from a reference to the implementation, to the generic trait object. In this case with a map containing a cast. I had tried using into, but there doesn't seem to be an implementation for convert between a reference to the implementor of a trait and a reference to the trait object.

In this example, file() returns a Vec<u8> as the underlying writer, I did this so that the solution was testable in a Rust playground, but this could as well be a File.

Finally, I removed the lifetime constraint on the trait object, the type system/borrow checker didn't seem to think it was necessary.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
amnn
  • 3,657
  • 17
  • 23
  • 1
    *removed the lifetime constraint on the trait object* — [Why is adding a lifetime to a trait with the plus operator (Iterator + 'a) needed?](https://stackoverflow.com/q/42028470/155423) – Shepmaster Apr 20 '19 at 13:11
  • 1
    You are allowed to read and write to the filesystem in the playground. – Shepmaster Apr 20 '19 at 13:39
  • @Shepmaster, that's good to know! Here is a [Rust playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=66c62e25e7a496706390b68364afd092) using a file. – amnn Apr 20 '19 at 16:01
0

Thanks for the answers above. Prior to them being posted I had an idea myself and came up with this (cut down to a minimal example that shows more or less what I'm trying to do):

use std::{env, process};
use std::fs::File;
use std::io::{self, BufWriter};

pub struct Parameters {
    pub log: Option<Box<dyn io::Write>>,
}


fn main() {
    let args: Vec<String> = env::args().collect();
    let logfile = if args.len() > 1 {
        Some(&args[1])
    } else {
        None
    };

    let logstream: Option<Box<dyn io::Write>> = match logfile {
        Some(logfile) => {
            let f = File::create(logfile).unwrap_or_else(|err| {
                eprintln!("ERROR opening logfile: {}", err);
                process::exit(1);
            });
            let logstream = BufWriter::new(f);
            Some(Box::new(logstream))
        }
        None => {
            None
        },
    };

    let parameters = Parameters {
        log: logstream,
    };
    play(parameters);
}

fn play(parameters: Parameters) {
    // Rest of code removed
    if let Some(mut stream) = parameters.log {
        stream.write("Testing\n".as_bytes()).unwrap_or_else(|err| {
            eprintln!("ERROR writing to logfile: {}", err);
            process::exit(1);
        });
    }
}

Is this a good solution or would one of the ones suggested above be better?

Bob
  • 1,037
  • 1
  • 9
  • 14
  • 1
    This doesn't answer the question as originally posed: *How to put a **reference** to a trait object in an `Option`?* – Shepmaster Apr 20 '19 at 20:11
  • 1
    Rust has an extremely rich standard library that captures a lot of the idioms that you've written from scratch here. For example, you can use `env::args().nth(1)` to optionally get the 1st argument, and rather than using `process::exit`, I would change the return type of `main` to `io::Result<()>` and use the `?` operator to propagate the error. Here's an adapted [Rust playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=fe8288d55cf505f1c156362a6b7e1141) – amnn Apr 21 '19 at 10:23
  • @Shepmaster - Thanks for your help. Maybe this doesn't really answer the question as written, though as I understand it a Box is a type of reference to an object on the heap. Maybe my question wasn't clear enough as this did achieve what I needed - to optionally create the writer externally to my library and pass it in. – Bob Apr 24 '19 at 05:24
  • @amnn - Thanks for the tip about args().nth(1) - I'm still learning Rust and the library is large so it takes a while to realise these things exist. As for the process::exit() - yes, that was just for a quick-and-dirty demo - usually I would return a Result() object and use ? (I use ? quite extensively in my real code) – Bob Apr 24 '19 at 05:26