-
Notifications
You must be signed in to change notification settings - Fork 627
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
Add futures-io #780
Conversation
/// 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 { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
onprepare_uninitialized_buffer
being unsafe to implement, not unsafe to use (the more common, and arguably the correct meaning ofunsafe 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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}; |
There was a problem hiding this comment.
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}; |
There was a problem hiding this comment.
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.
futures-util/src/io/mod.rs
Outdated
return Ok(Async::Ready(0)); | ||
} | ||
|
||
let n = try_ready!(self.poll_write(buf.bytes(), cx)); |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
For the record, Tokio will continue to provide it's own I/O is fundamental to Tokio as such it needs to maintain its own traits (they will probably diverge some). |
@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 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. |
@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 But yes, as you mentioned, one will always be able to write adapters. |
There was a problem hiding this 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.
futures-io/src/lib.rs
Outdated
/// 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Currently, a few combinators have been disabled because they rely on
BufRead
capabilities. We should probably add anAsyncBufRead
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.