Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add forwarding impls for Read, Write, Seek to Arc, Rc #93044

Closed
wants to merge 5 commits into from

Conversation

eholk
Copy link
Contributor

@eholk eholk commented Jan 18, 2022

This adds forwarding impls for std::io::Read, std::io::Write, and std::io::Seek to alloc:sync::Arc and alloc::rc::Rc. This is relevant for types such as std::fs::File, std::net::TcpStream, and std::os::unix::UnixStream which implement Read, Write, and Seek for &T.

It is currently possible to do this manually through wrappers (See the "Implement a forwarding wrapper by hand" section for an example), but providing forwarding impls makes this pattern nicer to use. In some cases this can also be done with an extra reference, as shown below:

let stream = TcpStream::connect("localhost:8080");
let stream = Arc::new(stream);

&stream1.write(b"hello world")?; // OK: Read is available for &Arc<TcpStream>.
stream1.write(b"hello world")?;  // Error: Read is not available for Arc<TcpStream>.
                                 // (Enabled by this PR)

The reason why we want Arc<T>: Read where &T: Read is because this enables Arc<T> to be passed directly into APIs which expect T: Read:

fn operate<R: Read>(reader: R) {}

let stream = TcpStream::connect("localhost:8080");
let stream = Arc::new(stream);
operate(stream);  // Error: `Arc<TcpStream>` does not implement `Read`
                  // (Enabled by this PR)

Implement a forwarding wrapper by hand

These trait impls do not allow anything which wasn't allowed before. Dereferencing to the inner type to get Arc<T>: Read to work is already possible by creating an intermediate type, as shown here:

/// A variant of `Arc` that delegates IO traits if available on `&T`.
#[derive(Debug)]
pub struct IoArc<T>(Arc<T>);

impl<T> IoArc<T> {
    /// Create a new instance of IoArc.
    pub fn new(data: T) -> Self {
        Self(Arc::new(data))
    }
}

impl<T> Read for IoArc<T>
where
    for<'a> &'a T: Read,
{
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        (&mut &*self.0).read(buf)
    }
}

impl<T> Write for IoArc<T>
where
    for<'a> &'a T: Write,
{
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        (&mut &*self.0).write(buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        (&mut &*self.0).flush()
    }
}

// forward `Clone`, `Seek` as well here

References

Joint work with @yoshuawuyts.

@rust-highfive
Copy link
Collaborator

r? @m-ou-se

(rust-highfive has picked a reviewer for you, use r? to override)

@rust-highfive rust-highfive added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Jan 18, 2022
@nrc nrc added the T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. label Jan 19, 2022
Copy link
Member

@nrc nrc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good addition, but it desperately needs some documentation! I'm not sure where the best place for that is, but either on the types or the impls or somewhere. I think it needs explaining that these impls exist, what they allow, how they can be used, some examples, etc. There should also be comments (i.e., not user-facing docs) explaining why the bound on the impl is the way it is (why not T: Read, etc) and probably a comment explaining why these are on Arc and Rc, not &Arc and &Rc.

@rust-log-analyzer

This comment has been minimized.

@eholk
Copy link
Contributor Author

eholk commented Jan 26, 2022

@nrc - thanks for the suggestion! I added some doc comments on the impls, which I think also does a reason for the bound, etc. Let me know if you think this is clear enough, and if not I can add more detail.

Copy link
Member

@nrc nrc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm, thanks for the docs!

@m-ou-se m-ou-se added needs-fcp This change is insta-stable, so needs a completed FCP to proceed. S-waiting-on-fcp Status: PR is in FCP and is awaiting for FCP to complete. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Mar 9, 2022
@m-ou-se
Copy link
Member

m-ou-se commented Mar 9, 2022

@rfcbot merge

@rfcbot
Copy link

rfcbot commented Mar 9, 2022

Team member @m-ou-se has proposed to merge this. The next step is review by the rest of the tagged team members:

Concerns:

Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot rfcbot added proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels Mar 9, 2022
@m-ou-se
Copy link
Member

m-ou-se commented Mar 9, 2022

At first glance seems to have the same issue as pointed out here: #94744 (comment), but the implementation in this PR implicitly requires Sized, meaning that T = [u8] is not accepted.

@tbu-
Copy link
Contributor

tbu- commented Mar 9, 2022

At first glance seems to have the same issue as pointed out here: #94744 (comment), but the implementation in this PR implicitly requires Sized, meaning that T = [u8] is not accepted.

Sounds like this should be explicitly documented as a pitfall as T = [u8] is probably not the only possible Read etc. implementation which might have this problem.

@dtolnay
Copy link
Member

dtolnay commented Mar 9, 2022

It's not clear to me that this PR is less problematic than #94744. The relevant distinction in #94744 was whether the impls mutate the incoming reference or not, not whether the reference is fat. Ruling out fat references as done here doesn't rule out impls which work by mutating the reference, in general.

Here is one counterexample:

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

#[derive(Debug)]
enum Parity {
    Even,
    Odd,
}

impl Write for &Parity {
    fn write(&mut self, buf: &[u8]) -> Result<usize> {
        if buf.iter().map(|x| x.count_ones()).sum::<u32>() % 2 == 1 {
            match self {
                Parity::Even => *self = &Parity::Odd,
                Parity::Odd => *self = &Parity::Even,
            }
        }
        Ok(buf.len())
    }

    fn flush(&mut self) -> Result<()> {
        Ok(())
    }
}

fn main() {
    let mut parity = &Parity::Even;
    write!(parity, "###").unwrap();
    println!("{:?}", parity);  // Odd
    write!(parity, "###").unwrap();
    println!("{:?}", parity);  // Even
}

Are we saying these impls are not always correct but still useful enough in practice to include anyway?

@rfcbot concern behavior on impls that mutate the reference

@eholk
Copy link
Contributor Author

eholk commented Mar 9, 2022

Thanks for the example, @dtolnay. I think that's a really clear illustration of the issue. Here's what it looks like with this PR, going through an ARC:

use std::io::{Result, Write};
use std::sync::Arc;

#[derive(Debug)]
enum Parity {
    Even,
    Odd,
}

impl Write for &Parity {
    fn write(&mut self, buf: &[u8]) -> Result<usize> {
        if buf.iter().map(|x| x.count_ones()).sum::<u32>() % 2 == 1 {
            match self {
                Parity::Even => *self = &Parity::Odd,
                Parity::Odd => *self = &Parity::Even,
            }
        }
        Ok(buf.len())
    }

    fn flush(&mut self) -> Result<()> {
        Ok(())
    }
}

fn main() {
    let mut parity = Arc::new(Parity::Even);
    write!(parity, "###").unwrap();
    println!("{:?}", parity);  // Even
    write!(parity, "###").unwrap();
    println!("{:?}", parity);  // Even
}

Basically, the write! has no effect.

This feels like the sort of thing that should require unsafe to make happen, although I can see why it doesn't. It definitely looks like a footgun to me.

I'd love to find a more generic way of doing this than specializing for File, AsyncStream, and every other type that makes sense here. I guess we could add something like an InteriorMutabilityIO marker trait to make it easier to opt in, but that seems kind of ugly too.

One argument in favor of still merging this is that it doesn't do anything users can't already do on their own. That said, I think it makes sense to avoid adding known footguns to the standard library if we can avoid it.

@m-ou-se
Copy link
Member

m-ou-se commented May 18, 2022

It definitely looks like a footgun to me.

@rfcbot abort

@dtolnay
Copy link
Member

dtolnay commented May 18, 2022

@rfcbot cancel

@rfcbot
Copy link

rfcbot commented May 18, 2022

@dtolnay proposal cancelled.

@rfcbot rfcbot removed proposed-final-comment-period Proposed to merge/close by relevant subteam, see T-<team> label. Will enter FCP once signed off. disposition-merge This issue / PR is in PFCP or FCP with a disposition to merge it. labels May 18, 2022
@Dylan-DPC Dylan-DPC added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-fcp Status: PR is in FCP and is awaiting for FCP to complete. labels Jun 4, 2022
@WaffleLapkin
Copy link
Member

Since the proposal was canceled, should this PR be closed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs-fcp This change is insta-stable, so needs a completed FCP to proceed. S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging this pull request may close these issues.