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 futures-io #780

Merged
merged 2 commits into from
Feb 21, 2018
Merged

Add futures-io #780

merged 2 commits into from
Feb 21, 2018

Conversation

cramertj
Copy link
Member

@cramertj cramertj commented Feb 20, 2018

Currently, a few combinators have been disabled because they rely on BufRead capabilities. We should probably add an AsyncBufRead in order to bring back this functionality, but I have not yet done so.

I have also not yet ported the tests from the original repo. I plan to add these in a future commit.

/// methods, and that the return value of the method accurately reflects
/// the number of bytes that have been written to the head of the buffer.
#[inline]
pub unsafe fn nop() -> Initializer {
Copy link
Contributor

Choose a reason for hiding this comment

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

What is nop? Why not use a descriptive name, like the alternate constructor, zeroing?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is done to match the current (unstable) std API. When that API is released, this Iniitalizer will be changed to a type alias to that one: https://doc.rust-lang.org/std/io/struct.Initializer.html

Copy link
Contributor

Choose a reason for hiding this comment

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

The name nop seems unfortunate. Regardless of the previous naming choice of an unstable std API, it seems better to pick a better name in a new library.

/// return a non-zeroing `Initializer` from another `AsyncRead` type
/// without an `unsafe` block.
#[inline]
unsafe fn initializer(&self) -> Initializer {
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought the point of the Initializer having an unsafe constructor was so that this method didn't need to be unsafe?

Copy link
Member Author

Choose a reason for hiding this comment

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

Initializer::zeroing() is safe, Initializer::nop() is unsafe. It is safe to implement this method using Initializer::zeroing(), but unsafe to implement it using Initializer::nop().

This function is made unsafe so that users cannot implement it safely by delegating to the internally-unsafe implementation of the initializer function on another type.

All of this is copied from the std::io API. If you have comments, questions, or ideas about how to improve this design, please leave them in the tracking issue. If that API changes, we will update this one as necessary.

Copy link
Contributor

Choose a reason for hiding this comment

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

Quoting from the 0.2 RFC:

This resolves the issue of the unsafe on prepare_uninitialized_buffer being unsafe to implement, not unsafe to use (the more common, and arguably the correct meaning of unsafe fn).

The implementation doesn't seem to match, since even if Initializer::zeroing() is returned from fn initializer(), it doesn't matter, because it's unsafe to call initializer().

I'm leaving comments here because this API is being proposed in futures, a library that I rely on, and a place where stability markers cannot protect the it. The tracking issue in std hasn't seen much traction otherwise.

Copy link
Member Author

@cramertj cramertj Feb 21, 2018

Choose a reason for hiding this comment

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

This PR does indeed address the issue I raised in the text you quoted. However, I think the wording I used may have been confusing.

Currently, unsafe fn prepare_uninitialized_buffer is unsafe to implement, but not unsafe to call. unsafe on functions is intended to mean "unsafe to call", not "unsafe to implement." If an RFC like rust-lang/rfcs#2316 were to be approved, the prepare_uninitialized_buffer API would be unsound.

This PR fixes this problem by moving the unsafety to the call to Initializer::nop(). Even if rust-lang/rfcs#2316 were approved, this API would be sound. The initializer function itself is still unsafe, but the reason is different: it isn't that initializer is unsafe to implement, but that calling initializer is unsafe because it could allow you to produce values of type Initializer::nop() even when your AsyncRead implementation read from the supplied buffer.

Copy link
Contributor

Choose a reason for hiding this comment

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

Currently, unsafe fn prepare_uninitialized_buffer is unsafe to implement, but not unsafe to call.

I assume you mean that is the intent, since it definitely is unsafe to call the function with the current compilers.

I find the reasoning that you could get an Initializer::nop() back a little bit arbitrary: you don't find this sort of unsafe "poisoning" elsewhere. For instance, you can get a string back with str::from_utf8_unchecked(), but that shouldn't mean that any function that happens to wrap that is inherently unsafe.

Copy link
Member Author

Choose a reason for hiding this comment

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

Consider this implementation:

struct MyUnsafePerformantRead;

impl AsyncRead for MyUnsafePerformantRead {
    fn initializer(&self) -> Initializer {
        unsafe { Initializer::nop() }
    }
    fn poll_read.... // Doesn't read from the buffer
}

struct MySafeButStillUninitRead;

impl AsyncRead for MySafeButStillUninitRead {
    fn initializer&self) -> Initializer {
        MyUnsafePerformantRead.initializer()
    }
    fn poll_read.... // Does read from the buffer (uninitialized memory), causes UB
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I imagined that is the concern, but I don't see anything that adding an unsafe to the function will help with. In fact, adding the unsafe keywords doesn't protect it at all. A wrong implementation is still wrong.

Copy link
Member Author

@cramertj cramertj Feb 21, 2018

Choose a reason for hiding this comment

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

unsafe prevents the unsound implementation of MySafeButStillUninitRead (undefined behavior without unsafe code). If MyUnsafeButPerformantRead::initializer() is unsafe, then, MySafeButStillUninitRead::initializer would have to use unsafe in order to trigger the UB.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm even more unconvinced now, since you can easily grab an Initializer from a completely unrelated reader, and carry it around, and safely use it via unrelated_initializer.initialize(my_buf) everywhere else, and trigger the UB.

As a not-arbitrary example, consider Chain implementation takes Box<AsyncRead>s, and pushes them into a vec. The naive implementation calls self.reads[0].initializer(), and saves it, and then uses it in later reads, instead of remembering to ask for a new one.

I think the std API gives more an illusion of protection against UB, while still quite easily allowing it to happen, even by mistake.

Copy link
Member Author

Choose a reason for hiding this comment

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

since you can easily grab an Initializer from a completely unrelated reader, and carry it around

Not without using unsafe. If you're making unsafe calls and then randomly passing around the result, in explicit violation of the invariants stated in the docs, then you're right, it is easy to cause UB.

@@ -0,0 +1,373 @@
//! Utilities for encoding and decoding frames.
Copy link
Contributor

Choose a reason for hiding this comment

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

All of the codec stuff seems out of scope.

@@ -0,0 +1,124 @@
use bytes::{Bytes, BufMut, BytesMut};
Copy link
Contributor

Choose a reason for hiding this comment

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

This codecs stuff also seems out of scope.

@@ -0,0 +1,257 @@
use std::io::{self, Read, Write};
Copy link
Contributor

Choose a reason for hiding this comment

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

This framed/codec stuff seems out of scope.

return Ok(Async::Ready(0));
}

let n = try_ready!(self.poll_write(buf.bytes(), cx));
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems the default implementation here could make use of self.poll_vectored_write.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done! Thanks for the reminder :)

/// A type used to conditionally initialize buffers passed to `AsyncRead`
/// methods.
#[derive(Debug)]
pub struct Initializer(bool);
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at the std Read::initializer tracking issue, it seems there is doubt that this is the correct API. In std, the unstable attribute protects them.

It seems odd to be using that API here if it's thought to be inferior, especially since we can't rely on unstable to protect against breaking changes.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's true that some people have doubts about the API. I personally think that it's the best-designed interface for this that I've seen, and it's the only one I've seen that is sound in the presence of rust-lang/rfcs#2316.

@carllerche
Copy link
Member

For the record, Tokio will continue to provide it's own AsyncRead / AsyncWrite traits as well as all the utilities that were copied from the Tokio source.

I/O is fundamental to Tokio as such it needs to maintain its own traits (they will probably diverge some).

@aturon
Copy link
Member

aturon commented Feb 20, 2018

@carllerche I continue to see that as a rather unfortunate decision, since a basic goal here is to provide interoperation between different backing I/O systems, and basic implementation-agnostic conveniences -- exactly akin to std::io. I'm not aware of anything on a technical level that would prevent Tokio from using these traits. But I suspect it's fruitless to try to argue this point further.

In any case, the good news is that Rust provides ample means for "external" interoperation (through newtype wrappers and the like), which means the Tokio team's stance here will merely lead to increased friction, rather than total inability to interoperate.

@carllerche
Copy link
Member

@aturon Tokio already provides traits for interop that are decoupled from the reactor. I'm not sure what the value is in renaming the lib from tokio-io to futures-io and creating an additional standard instead of making the changes in the existing tokio-io crate, especially since it looks like you are copying the contents.

But yes, as you mentioned, one will always be able to write adapters.

Copy link
Member

@aturon aturon left a comment

Choose a reason for hiding this comment

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

This basically LGTM, modulo small convention issue I mentioned.

As to the initializer stuff: I believe that we should match what std does, as planned, and address any issues upstream, pushing toward stabilization. Once that's done, we can adopt any changes here in e.g. 0.3.

/// If reading would block, this function returns `Ok(Async::Pending)`
/// and arranges for `cx.waker()` to receive a notification when the
/// object becomes readable or is closed.
fn poll_read(&mut self, buf: &mut [u8], cx: &mut task::Context)
Copy link
Member

Choose a reason for hiding this comment

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

As per IRC discussion, let's make cx come first, consistently.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@cramertj cramertj merged commit c1297ec into rust-lang:0.2 Feb 21, 2018
@cramertj cramertj deleted the futures-io branch February 21, 2018 05:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants