├── .gitignore ├── explainer └── README.md ├── meetings └── README.md ├── CODE_OF_CONDUCT.md ├── .github ├── ISSUE_TEMPLATE │ ├── question.md │ ├── proposal.md │ ├── bug-report.md │ └── experience-report.md └── workflows │ └── deploy_mdbook.yml ├── SUMMARY.md ├── FAQ.md ├── explainer.md ├── evaluation.md ├── updates.md ├── RFC.md ├── book.toml ├── CHARTER.md ├── io-traits ├── alternatives │ ├── split-trait.md │ ├── tweaks.md │ ├── async-traits.md │ └── super-ready.md └── README.md ├── updates └── template.md ├── LICENSE-MIT ├── README.md └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /book 2 | -------------------------------------------------------------------------------- /explainer/README.md: -------------------------------------------------------------------------------- 1 | # Explainer 2 | -------------------------------------------------------------------------------- /meetings/README.md: -------------------------------------------------------------------------------- 1 | # Meetings 2 | 3 | This folder contains the minutes of all the recorded meetings that have happened 4 | so far. 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # The Rust Code of Conduct 2 | 3 | The Code of Conduct for this repository [can be found online](https://www.rust-lang.org/conduct.html). 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask for clarification about some aspect of the design 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please describe what you want to know! -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [👋 Welcome](./README.md) 4 | - [✏️ Updates](./updates.md) 5 | - [📜 Charter](./CHARTER.md) 6 | - [🔬 Evaluation](./evaluation.md) 7 | - [📚 Explainer](./explainer.md) 8 | - [✨ RFC](./RFC.md) 9 | - [😕 FAQ](./FAQ.md) 10 | -------------------------------------------------------------------------------- /.github/workflows/deploy_mdbook.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | jobs: 7 | ci: 8 | name: CI 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: XAMPPRocky/deploy-mdbook@v1 13 | with: 14 | token: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /FAQ.md: -------------------------------------------------------------------------------- 1 | # 😕 Frequently asked questions 2 | 3 | > This page lists frequently asked questions about the design. It often redirects to the other pages on the site. 4 | 5 | ## What is the goal of this initiative? 6 | 7 | See the [Charter](./CHARTER.md). 8 | 9 | ## Who is working on it! 10 | 11 | See the [Charter](./CHARTER.md). 12 | -------------------------------------------------------------------------------- /explainer.md: -------------------------------------------------------------------------------- 1 | # 📚 Explainer 2 | 3 | > The "explainer" is "end-user readable" documentation that explains how to use the feature being deveoped by this initiative. 4 | > If you want to experiment with the feature, you've come to the right place. 5 | > Until the feature enters "feature complete" form, the explainer should be considered a work-in-progress. -------------------------------------------------------------------------------- /evaluation.md: -------------------------------------------------------------------------------- 1 | # 🔬 Evaluation 2 | 3 | > The *evaluation* surveys the various design approaches that are under consideration. 4 | It is not required for all initiatives, only those that begin with a problem statement 5 | but without a clear picture of the best solution. Often the evaluation will refer to topics 6 | in the [design-discussions] for more detailed consideration. 7 | 8 | [design-discussions]: ./design-discussions.md -------------------------------------------------------------------------------- /updates.md: -------------------------------------------------------------------------------- 1 | # ✏️ Updates 2 | 3 | Lang-team initiatives give monthly updates. This section collects the updates from this initiative for posterity. 4 | 5 | To add a new update: 6 | 7 | * Create a new file `updates/YYYY-mmm.md`, e.g. `updates/2021-nov.md` 8 | * We recomend basing this on the [update template] 9 | * Link it from the `SUMMARY.md` 10 | 11 | [update template]: https://github.com/rust-lang/initiative-template/tree/master/updates/template.md -------------------------------------------------------------------------------- /RFC.md: -------------------------------------------------------------------------------- 1 | # ✨ RFC 2 | 3 | > The RFC exists here in draft form. It will be edited and amended over the course of this initiative. 4 | > Note that some initiatives produce multiple RFCs. 5 | > 6 | > Until there is an accepted RFC, any feature gates must be labeled as experimental. 7 | > 8 | > When you're ready to start drafting, copy in the [template text](https://raw.githubusercontent.com/rust-lang/rfcs/master/0000-template.md) from the [rfcs](https://github.com/rust-lang/rfcs) repository. -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["{{INITIATIVE_NAME}} Members"] 3 | language = "en" 4 | multilingual = false 5 | src = "." 6 | title = "{{INITIATIVE_NAME}} initiative" 7 | 8 | [output.html] 9 | no-section-label=true 10 | git-repository-url="https://github.com/rust-lang/{{INITIATIVE_SLUG}}" 11 | edit-url-template="https://github.com/rust-lang/{{INITIATIVE_SLUG}}/edit/master/{path}" 12 | site-url="/{{INITIATIVE_SLUG}}/" 13 | 14 | [output.html.fold] 15 | enable = true 16 | level = 0 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/proposal.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Proposal 3 | about: Propose a change to the way this feature works 4 | title: '' 5 | labels: proposal 6 | assignees: '' 7 | 8 | --- 9 | 10 | **When proposing changes, please check the FAQ and design questions to see if this idea has already been considered. It's also a good idea to check with the owner first.** 11 | 12 | ### Motivation 13 | 14 | Describe what you're trying to accomplish -- what problem are you trying to solve? 15 | 16 | ### Details 17 | 18 | Describe your idea. 19 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug using this feature 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ### When using 10 | 11 | Please indicate the precise nightly build you are using; try `rustc --version` 12 | 13 | ### What I tried to do 14 | 15 | Please include sample code or links to your project! 16 | 17 | ### What happened 18 | 19 | Describe what happened in as much detail as possible. 20 | For compiler ICEs, a good idea would be to include the output with `RUST_BACKTRACE=1`. 21 | 22 | ### What I expected 23 | 24 | Describe what you thought should happen. 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/experience-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Experience report 3 | about: Describe your experiences using this feature 4 | title: '' 5 | labels: experience-report 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Code description 11 | 12 | Describe what you were trying to do with generic associated types. Links to source code are always appreciated! 13 | 14 | ### What worked well 15 | 16 | Describe what aspects of GATs worked well for you. 17 | 18 | ### What worked less well 19 | 20 | Describe what aspects of GATs did not work well for you. Perhaps they were confusing, or you weren't able to get your code to compile the way you wanted? Or perhaps it was just not ergonomic. 21 | -------------------------------------------------------------------------------- /CHARTER.md: -------------------------------------------------------------------------------- 1 | # 📜 {{INITIATIVE_NAME}} Charter 2 | 6 | 7 | ## Proposal 8 | 9 | 15 | 16 | 17 | ## Membership 18 | 19 | | Role | Github | 20 | | --- | --- | 21 | | [Owner] | [ghost](https://github.com/ghost) | 22 | | [Liaison] | [ghost](https://github.com/ghost) | 23 | 24 | [Owner]: https://lang-team.rust-lang.org/initiatives/process/roles/owner.html 25 | [Liaison]: https://lang-team.rust-lang.org/initiatives/process/roles/liaison.html 26 | -------------------------------------------------------------------------------- /io-traits/alternatives/split-trait.md: -------------------------------------------------------------------------------- 1 | # Alternative: `Split` trait 2 | 3 | ```rust 4 | // Ready, Read, and Write are unchanged. 5 | 6 | trait Split: Read + Write { 7 | async fn split(&mut self) -> (impl Read + '_, impl Write + '_) 8 | } 9 | ``` 10 | 11 | An alternative to implementing the `Read` and `Write` traits on reference types to support simultaneous reading and writing, is to have a `Split` trait which is used to split a resource into a reader and writer (which implement `Read` and `Write`, respectively). The advantage of this approach is that it supports reading and writing without simultaneous multiple reads (or writes). However, it is a more complicated API requiring another trait and extra concrete types for many resources, and brings us further from the sync traits. 12 | -------------------------------------------------------------------------------- /updates/template.md: -------------------------------------------------------------------------------- 1 | # YYYY-MMM: Lang team update 2 | 3 | ## Summary 4 | 5 | * Summarizes the key points here! 6 | * These should be in a format suitable to be copied-and-pasted into a "lang team monthly updates" blog post. 7 | * This is great place to give a shout out to contributors. 8 | 9 | ## Goals for this month 10 | 11 | * Outline the things you would like to achieve this month 12 | * Best is if you can make these fairly concrete: 13 | * "Extend evaluation doc to cover XXX" 14 | * "Find a solution for #12345" 15 | 16 | ## Questions for discussion, meeting proposals 17 | 18 | * Outline anything where you would like feedback from the broader lang team or community 19 | 20 | ## (Other topic) 21 | 22 | * Add additional `##` headings for topics that you'd like to cover in more detail. 23 | * For example, you could add a section to dive into details from the summary, or to explain a new approach that you decided to take (and why). 24 | * Please include links into the explainers, evaluation, github issues, etc where applicable! -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /io-traits/alternatives/tweaks.md: -------------------------------------------------------------------------------- 1 | # Tweaks 2 | 3 | These are smaller changes to the proposed design in [README.md](README.md), rather than full alternatives. 4 | 5 | They both have the same advantage: that they are arguably more logical designs for the `Read` trait, but also the same disadvantage: they take the async trait design further from the sync version. 6 | 7 | ## Make vectored IO a separate trait 8 | 9 | It was arguably a mistake to add vectored IO to the existing IO traits rather than making them separate traits, the `is_vectored` method indicates this. We could fix that for async IO, at the expense of some similarity to the sync traits. 10 | 11 | ```rust 12 | pub use std::io::Result; 13 | 14 | pub trait Ready { 15 | async fn ready(&mut self, interest: Interest) -> Result; 16 | } 17 | 18 | pub trait Read: Ready { 19 | fn non_blocking_read_buf(&mut self, buf: &mut ReadBuf<'_>) -> Result>; 20 | 21 | async fn read(&mut self, buf: &mut [u8]) -> Result { ... } 22 | async fn read_buf(&mut self, buf: &mut ReadBuf<'_>) -> Result<())> { ... } 23 | async fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> { ... } 24 | async fn read_buf_exact(&mut self, buf: &mut ReadBuf<'_>) -> Result<()> { ... } 25 | async fn read_to_end(&mut self, buf: &mut Vec) -> Result { ... } 26 | async fn read_to_string(&mut self, buf: &mut String) -> Result { ... } 27 | 28 | fn by_ref(&mut self) -> &mut Self 29 | where 30 | Self: Sized, 31 | { ... } 32 | } 33 | 34 | pub trait ReadVectored: Ready { 35 | fn non_blocking_read_buf_vectored(&mut self, bufs: &mut ReadBufVec<'_>) -> Result>; 36 | 37 | async fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> Result { ... } 38 | async fn read_buf_vectored(&mut self, bufs: &mut ReadBufVec<'_>) -> Result { ... } 39 | 40 | fn by_ref(&mut self) -> &mut Self 41 | where 42 | Self: Sized, 43 | { ... } 44 | } 45 | 46 | pub trait Write: Ready { 47 | fn non_blocking_write(&mut self, buf: &[u8]) -> Result>; 48 | fn non_blocking_flush(&mut self) -> Result>; 49 | 50 | async fn write(&mut self, buf: &[u8]) -> Result { ... } 51 | async fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... } 52 | async fn write_fmt(&mut self, fmt: Arguments<'_>) -> Result<()> { ... } 53 | async fn flush(&mut self) -> Result<()> { ... } 54 | 55 | fn by_ref(&mut self) -> &mut Self 56 | where 57 | Self: Sized, 58 | { ... } 59 | } 60 | 61 | pub trait WriteVectored: Ready { 62 | fn non_blocking_write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result>; 63 | fn non_blocking_flush(&mut self) -> Result>; 64 | 65 | async fn write_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result { ... } 66 | async fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<()> { ... } 67 | async fn flush(&mut self) -> Result<()> { ... } 68 | 69 | fn by_ref(&mut self) -> &mut Self 70 | where 71 | Self: Sized, 72 | { ... } 73 | } 74 | 75 | // No change to helper types 76 | ``` 77 | 78 | ## Only provide `ReadBuf` methods 79 | 80 | Rather than providing methods which operate on `ReadBuf` and `&[u8]` we could elide the latter since it is easy to convert from `ReadBuf` to `&[u8]`. Alternatively we could have the methods take `impl Into` thus having a single method which can take both types (although this prevents using the IO traits as trait objects). 81 | 82 | ```rust 83 | pub trait Read: Ready { 84 | fn non_blocking_read(&mut self, buf: &mut ReadBuf<'_>) -> Result>; 85 | fn non_blocking_read_vectored(&mut self, bufs: &mut ReadBufVec<'_>) -> Result> { ... } 86 | 87 | async fn read(&mut self, buf: &mut ReadBuf<'_>) -> Result<())> { ... } 88 | async fn read_exact(&mut self, buf: &mut ReadBuf<'_>) -> Result<()> { ... } 89 | async fn read_vectored(&mut self, bufs: &mut ReadBufVec<'_>) -> Result { ... } 90 | async fn read_to_end(&mut self, buf: &mut Vec) -> Result { ... } 91 | async fn read_to_string(&mut self, buf: &mut String) -> Result { ... } 92 | 93 | fn is_read_vectored(&self) -> bool { ... } 94 | 95 | fn by_ref(&mut self) -> &mut Self 96 | where 97 | Self: Sized, 98 | { ... } 99 | } 100 | 101 | // No change to `Write` or `Ready` trait of utility types. 102 | ``` 103 | 104 | ## No impls on reference types 105 | 106 | We could encourage implementers not to implement `Read` and `Write` for reference types. This would still allow simultaneous read/write via the `ready` method. However, it forces more users away from the ergonomic path. It is also less symmetric with the sync trait model. 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # async portability initiative 2 | 32 | 33 | ![initiative status: active](https://img.shields.io/badge/status-active-brightgreen.svg) 34 | 35 | ## Notes 36 | 37 | Key links about async portability. 38 | 39 | * [Repo](https://github.com/nrc/portable-interoperable) 40 | * [Vision roadmap](https://rust-lang.github.io/wg-async-foundations/vision/roadmap/portable.html) 41 | * [Roadmap goal tracking issue](https://github.com/rust-lang/wg-async-foundations/issues/259) 42 | * [Roadmap initiative tracking issue](https://github.com/rust-lang/wg-async-foundations/issues/260) 43 | 44 | Async foundations issues 45 | 46 | (See also status quo story issues) 47 | 48 | * [AsyncRead, AsyncWrite traits](https://github.com/rust-lang/wg-async-foundations/issues/23) 49 | 50 | ## What is this? 51 | 52 | This page tracks the work of the async portability [initiative]! To learn more about what we are trying to do, and to find out the people who are doing it, take a look at the [charter]. 53 | 54 | [charter]: ./CHARTER.md 55 | [initiative]: https://lang-team.rust-lang.org/initiatives.html 56 | 57 | ## Current status 58 | 59 | The following table lists of the stages of an initiative, along with links to the artifacts that will be produced during that stage. 60 | 61 | | Stage | State | Artifact(s) | 62 | | ------------------------------------- | ----- | ----------- | 63 | | [Proposal] | ✅ | [Proposal issue](https://github.com/rust-lang/lang-team/) | 64 | | | | [Charter](./CHARTER.md) | 65 | | | | [Tracking issue](https://github.com/rust-lang/rust/) | 66 | | [Experimental] | 🦀 | [Evaluation](./evaluation.md) | 67 | | | | [RFC](./RFC.md) | 68 | | [Development] | 💤 | [Explainer](./explainer.md) | 69 | | [Feature complete] | 💤 | Stabilization report | 70 | | [Stabilized] | 💤 | | 71 | 72 | [Proposal]: https://lang-team.rust-lang.org/initiatives/process/stages/proposal.html 73 | [Experimental]: https://lang-team.rust-lang.org/initiatives/process/stages/proposal.html 74 | [Development]: https://lang-team.rust-lang.org/initiatives/process/stages/development.html 75 | [Feature complete]: https://lang-team.rust-lang.org/initiatives/process/stages/feature-complete.html 76 | [Stabilized]: https://lang-team.rust-lang.org/initiatives/process/stages/stabilized.html 77 | 78 | Key: 79 | 80 | * ✅ -- phase complete 81 | * 🦀 -- phase in progress 82 | * 💤 -- phase not started yet 83 | 84 | ## How Can I Get Involved? 85 | 86 | * Check for 'help wanted' issues on this repository! 87 | * If you would like to help with development, please contact the [owner](./charter.md#membership) to find out if there are things that need doing. 88 | * If you would like to help with the design, check the list of active [design questions](./design-questions/README.md) first. 89 | * If you have questions about the design, you can file an issue, but be sure to check the [FAQ](./FAQ.md) or the [design-questions](./design-questions/README.md) first to see if there is already something that covers your topic. 90 | * If you are using the feature and would like to provide feedback about your experiences, please [open a "experience report" issue]. 91 | * If you are using the feature and would like to report a bug, please open a regular issue. 92 | 93 | We also participate on [Zulip][chat-link], feel free to introduce yourself over there and ask us any questions you have. 94 | 95 | [open issues]: /issues 96 | [chat-link]: https://rust-lang.zulipchat.com/#narrow/stream/187312-wg-async-foundations 97 | [team-toml]: https://github.com/rust-lang/team/blob/master/teams/initiative-{{INITIATIVE_SLUG}}.toml 98 | 99 | ## Building Documentation 100 | This repository is also an mdbook project. You can view and build it using the 101 | following command. 102 | 103 | ``` 104 | mdbook serve 105 | ``` 106 | -------------------------------------------------------------------------------- /io-traits/alternatives/async-traits.md: -------------------------------------------------------------------------------- 1 | # Alternative: async traits 2 | 3 | The simplest and most ergonomic alternative is to copy `io::{Read, Write}` and add `async` to most of the methods. I think this would work and would be optimal from the point of view of symmetry with the sync traits. However, it requires always pre-allocating a buffer per-read, which is unacceptable for some applications. The current proposal can be thought of as a super-set of the async-method-only proposal, from the perspective of the trait user. 4 | 5 | ```rust 6 | pub use std::io::Result; 7 | 8 | pub trait Read { 9 | async fn read(&mut self, buf: &mut [u8]) -> Result; 10 | async fn read_buf(&mut self, buf: &mut ReadBuf<'_>) -> Result<())> { ... } 11 | async fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> { ... } 12 | async fn read_buf_exact(&mut self, buf: &mut ReadBuf<'_>) -> Result<()> { ... } 13 | async fn read_buf_vectored(&mut self, bufs: &mut ReadBufVec<'_>) -> Result { ... } 14 | async fn read_to_end(&mut self, buf: &mut Vec) -> Result { ... } 15 | async fn read_to_string(&mut self, buf: &mut String) -> Result { ... } 16 | 17 | fn is_read_vectored(&self) -> bool { ... } 18 | 19 | fn by_ref(&mut self) -> &mut Self 20 | where 21 | Self: Sized, 22 | { ... } 23 | } 24 | 25 | pub trait Write: Ready { 26 | async fn write(&mut self, buf: &[u8]) -> Result; 27 | async fn write_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result { ... } 28 | async fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... } 29 | async fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<()> { ... } 30 | async fn write_fmt(&mut self, fmt: Arguments<'_>) -> Result<()> { ... } 31 | async fn flush(&mut self) -> Result<()>; 32 | 33 | fn is_write_vectored(&self) -> bool { ... } 34 | 35 | fn by_ref(&mut self) -> &mut Self 36 | where 37 | Self: Sized, 38 | { ... } 39 | } 40 | ``` 41 | 42 | ## Discussion 43 | 44 | Usage of this design matches the ergonomic usage of the proposed design. For implementers, things here are easier since they do not have to consider the `Ready` trait. 45 | 46 | The downside of this design is the constraints on how buffers must be managed. Lets look in detail at how `read` is used (the same arguments apply to the other reading and writing methods). The caller passes a borrowed buffer to `read`. Since `read` is async it may take some time to execute and the buffer is only used at the end of that time (when the IO read is ready to write data into the buffer), but the buffer must be allocated and borrowed by `read` for the whole duration of the call. That means that the buffer memory must be allocated and not used for that duration. In contrast, with the proposed approach, the buffer does not need to be passed into the IO system until the read is ready (when calling `non_blocking_read`, as the name suggests, this call should be quick). Whilst waiting for the IO operation to be ready (by calling `ready`), the buffer is not required. That means the buffer can be allocated just in time, or can be used for something else. 47 | 48 | Lets consider why this is important. Imagine some kind of HTTP server waiting on many requests. With this alternative, the server must allocate a lot of fairly large buffers - a significant usage of memory which is sitting uselessly while waiting for IO. With the `Ready` trait, there are multiple memory management strategies the server could use: 49 | 50 | * Allocate a buffer just in time for the read (no memory sits uselessly waiting); memory could be allocated on the stack for temporary usage or on the heap if the results of the read should be stored. 51 | * Use a single buffer for all reads, allocated in long-living memory or even statically. Not only is memory not sitting uselessly, but the server doesn't need to allocate on each call. The memory can only be used temporarily however since it will be required for the next read. 52 | * Use a buffer pool or similar strategy. Similar advantages to using a single buffer, but the buffers can be kept semi-permanently by clients. 53 | * Using a buffer which is part of an external data structure (this could be done under this alternative and I mention it since it is a scenario with different lifetimes which must be accommodated. One advantage of the `Ready` trait design is that even if the buffer is owned by an external object the memory itself can be allocated later leading to lower memory usage, e.g., if the buffer is a `Vec`, it can be owned by the external data structure but can be minimally sized until a read is ready when it can be expanded). 54 | 55 | ### Alternative buffer management designs 56 | 57 | Given that memory usage is important, how could we accommodate late allocation without the `Ready` trait (in particular, whilst keeping symmetry with the sync traits)? 58 | 59 | #### Don't 60 | 61 | We could just do nothing. However, at least one major runtime (Tokio) view supporting the memory-optimal path as a hard requirement, so if we choose not to support it then the async IO traits are unlikely to see widespread use. The requirement is also highlighted by end users (at AWS and Microsoft, at least) and by library authors (Hyper). Therefore, I believe some solution is necessary. 62 | 63 | #### Use a different buffer type 64 | 65 | We could either change the signature of `read` to use a different type or add methods to the `Read` trait. In either case, there are several possibilities for the buffer: 66 | 67 | * a concrete buffer type would include API for allocating the memory as well as writing into that memory so that the read method could allocate as late as possible, thus avoiding having memory 'sitting around'. Allocation could mean literally allocating or could mean checking out a buffer from a buffer pool. 68 | * A concrete buffer could refer to memory elsewhere (for example a single shared buffer, or buffers in other data structures). 69 | * The buffer type could be a trait permitting any of the previous strategies. However, if the trait is used generically it prevents using the `Read` trait as a trait object, and if the trait is used as a trait object then it is less efficient to use. 70 | 71 | Whether we use a trait or a concrete type, we must make a choice about whether the buffer is borrowed or owned. I don't think there is a single type that can be used which supports all buffer use cases and satisfies all other requirements for the `Read` trait design. In any case, we're still adding some API compared with the sync trait. 72 | 73 | This has the major downside that it is less ergonomic than the main proposal *and* is not symmetric with the sync system. 74 | 75 | #### Use `BufRead` and supply a growable buffer or buffer pool 76 | 77 | We could support only the straightforward `Read` trait and require using `BufRead` for memory-optimal use. That allows the buffer to be passed to the reader as the concrete type rather than in generic code which frees the implementation to handle buffers however it likes. The trouble with this approach is that it just pushes the difficult (impossible?) task of designing the buffer type to the IO library rather than std. For a general purpose IO library (or runtime), the task is no easier because there is no smaller scope. 78 | 79 | This approach has the additional downside that generic code which wants to support memory-optimal performance must accept `BufRead` instead of `Read` (or duplicate the functionality), even where `Read` would give a better experience for most users. On the other hand, it is the only solution which is truly symmetric with sync IO. I don't think that is enough to offset the downsides though. 80 | 81 | #### Use `Ready` trait in addition to the straightforward trait design 82 | 83 | In this alternative we'd have the straightforward `Read` and `Write` traits and have the `Ready` trait and `ReadyRead: Ready` and `ReadyWrite: Ready` as well. This would allow for a symmetric system and a separate memory-optimal system. However, it still means there is not symmetry between sync and async designs, and seems worse than just one set of IO traits. 84 | 85 | -------------------------------------------------------------------------------- /io-traits/alternatives/super-ready.md: -------------------------------------------------------------------------------- 1 | # Readiness super trait 2 | 3 | This was previously proposed as a favoured solution, however, it has the disadvantages of requiring all users to be aware of the readiness loop approach to IO. 4 | 5 | ## Read and Write proposal 6 | 7 | ```rust 8 | pub use std::io::Result; 9 | 10 | pub trait Ready { 11 | async fn ready(&mut self, interest: Interest) -> Result; 12 | } 13 | 14 | pub trait Read: Ready { 15 | fn non_blocking_read_buf(&mut self, buf: &mut ReadBuf<'_>) -> Result>; 16 | fn non_blocking_read_buf_vectored(&mut self, bufs: &mut ReadBufVec<'_>) -> Result> { ... } 17 | 18 | async fn read(&mut self, buf: &mut [u8]) -> Result { ... } 19 | async fn read_buf(&mut self, buf: &mut ReadBuf<'_>) -> Result<())> { ... } 20 | async fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> { ... } 21 | async fn read_buf_exact(&mut self, buf: &mut ReadBuf<'_>) -> Result<()> { ... } 22 | async fn read_buf_vectored(&mut self, bufs: &mut ReadBufVec<'_>) -> Result { ... } 23 | async fn read_to_end(&mut self, buf: &mut Vec) -> Result { ... } 24 | async fn read_to_string(&mut self, buf: &mut String) -> Result { ... } 25 | 26 | fn is_read_vectored(&self) -> bool { ... } 27 | 28 | fn by_ref(&mut self) -> &mut Self 29 | where 30 | Self: Sized, 31 | { ... } 32 | } 33 | 34 | pub trait Write: Ready { 35 | fn non_blocking_write(&mut self, buf: &[u8]) -> Result>; 36 | fn non_blocking_write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result> { ... } 37 | fn non_blocking_flush(&mut self) -> Result>; 38 | 39 | async fn write(&mut self, buf: &[u8]) -> Result { ... } 40 | async fn write_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result { ... } 41 | async fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... } 42 | async fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<()> { ... } 43 | async fn write_fmt(&mut self, fmt: Arguments<'_>) -> Result<()> { ... } 44 | async fn flush(&mut self) -> Result<()> { ... } 45 | 46 | fn is_write_vectored(&self) -> bool { ... } 47 | 48 | fn by_ref(&mut self) -> &mut Self 49 | where 50 | Self: Sized, 51 | { ... } 52 | } 53 | 54 | /// Express which notifications the user is interested in receiving. 55 | #[derive(Copy, Clone)] 56 | pub struct Interest(u32); 57 | 58 | /// Describes which operations are ready for an IO resource. 59 | #[derive(Copy, Clone)] 60 | pub struct Readiness(u32); 61 | 62 | impl Interest { 63 | pub const READ = ...; 64 | pub const WRITE = ...; 65 | pub const READ_WRITE = Interest(Interest::Read.0 | Interest::Write.0); 66 | } 67 | 68 | impl Debug for Interest { ... } 69 | 70 | impl Readiness { 71 | /// The resource is ready to read from. 72 | pub fn read(self) -> bool { ... } 73 | 74 | /// The resource is ready to write to. 75 | pub fn write(self) -> bool { ... } 76 | 77 | /// The resource has hung up. 78 | /// 79 | /// Note there may still be data to read. 80 | /// Note that the user does not *need* to check this method, even if the resource has hung up, 81 | /// the behaviour of `non_blocking_read` and `non_blocking_write` is defined and they should not 82 | /// panic. 83 | /// Note that the user does not need to request an interest in hup notifications, they may always 84 | /// be returned 85 | pub fn hup(self) -> bool { ... } 86 | } 87 | 88 | impl Debug for Readiness { ... } 89 | 90 | /// Whether an IO operation is ready for reading/writing or would block. 91 | #[derive(Copy, Clone, Debug, ...)] 92 | pub enum NonBlocking { 93 | Ready(T), 94 | WouldBlock, 95 | } 96 | 97 | /// A helper macro for handling the 'would block' case returned by `Ready::ready`. 98 | macro_rules! continue_on_block { 99 | ($e: expr) => { 100 | match $e { 101 | NonBlocking::Ready(t) => t, 102 | NonBlocking::WouldBlock => continue, 103 | } 104 | } 105 | } 106 | ``` 107 | 108 | I haven't included methods for iterating readers (`bytes`, `chain`, `take`), since work on async iterators is ongoing. I leave these for future work and do not believe they should block implementation or stabilisation. 109 | 110 | I've included methods for vectored reads into uninitialized memory (`read_buf_vectored`, etc.). These are not yet in sync `Read` trait but have been accepted as an RFC and are expected to be added soon. 111 | 112 | Some rationale: 113 | 114 | * The non-blocking read functions are only required for high performance usage and implementation. Therefore, there is no need for variations which take slices of bytes rather than a `ReadBuf` (used for reading into uninitialised memory, see [RFC 2930](https://www.ncameron.org/rfcs/2930)). Converting from a byte slice to a `ReadBuf` is trivial. 115 | * We elide async functions for vectored reads using byte slices - vectored IO is a high-performance option and the overhead of converting to a `ReadBuf` is trivial, so I don't think it is necessary. 116 | * The `read_exact` functions are convenience functions and by their nature it does not make sense to have non-blocking variations (c.f., the async functions) since they require multiple reads (and potentially waits). 117 | 118 | Note that we expect implementers to implement the provided non-blocking methods in some cases for optimal performance (e.g., for vectored IO). But expect the provided async methods to almost never be overridden. This will be documented. 119 | 120 | ### Usage - ergonomic path 121 | 122 | For the most ergonomic usage, where having the most optimal memory usage is not important, the async IO traits are used just like their sync counterparts. E.g., 123 | 124 | ```rust 125 | use std::async::io::{Read, Result}; 126 | 127 | async fn read_stream(stream: &TcpStream) -> Result<()> { 128 | let mut buf = [0; 1024]; 129 | let bytes_read = stream.read(&mut buf).await?; 130 | 131 | // we've read bytes_read bytes 132 | // do something with buf 133 | 134 | Ok(()) 135 | } 136 | ``` 137 | 138 | The important thing here is that asynchronous reading is exactly like synchronous reading, it just requires an await (and similarly for writing). 139 | 140 | ### Usage - memory-sensitive path 141 | 142 | Although closely matching the behaviour of synchronous IO is an important ergonomic goal, async IO does have different semantics around timing. Thus for absolutely optimal performance, it requires different usage patterns. In the above usage example, we allocate a buffer to read into, then call read, then await the result. In the sync world we would block until read returns. However, by using async and await we have a non-blocking wait which allows another task to allocate another buffer and await a different read. If we do this hundreds, thousands, or millions of times (which is what we want to be able to do with async IO), then that's a lot of memory allocated in buffers and not doing anything. Better behaviour would be to await the read and only allocate the buffer once the data is ready to be read. In this way we don't have a bunch of memory just sat around. In fact we could then just use a single buffer rather than being forced to allocate new ones! 143 | 144 | This usage pattern is facilitated by the `Ready` trait and the `non_blocking_` methods. For this usage, reading might look like: 145 | 146 | ```rust 147 | use std::async::io::{continue_on_block, Interest, Read, Ready, Result}; 148 | use std::io::ErrorKind; 149 | 150 | async fn read_stream(stream: &TcpStream) -> Result<()> { 151 | loop { 152 | stream.ready(Interest::READABLE).await?; 153 | 154 | let mut buf = vec![0; 1024]; 155 | let bytes_read = continue_on_block!(stream.non_blocking_read(&mut buf)?); 156 | 157 | // we've read bytes_read bytes 158 | // do something with buf 159 | 160 | return Ok(()); 161 | } 162 | } 163 | ``` 164 | 165 | This is less ergonomic than the previous example, but it is more memory-efficient. 166 | 167 | ### Implementations 168 | 169 | Implementers of the IO traits must implement `async::Ready` and either or both of `async::{Read, Write}`. They only need to implement the synchronous `non_blocking` method, the async method is provided (the default implementation follows the memory-sensitive usage example above). 170 | 171 | Where possible these traits should be implemented on reference types as well as value types (following the synchronous traits). E.g., all three traits should be implemented for both `TcpStream` and `&TcpStream`. This permits simultaneous reads and writes even when using the ergonomic path (simultaneous read/write is possible in the memory-optimal path without such implementations). 172 | 173 | Such implementations are not possible today in some runtimes due details of their waiting implementation. They would be ok with the traits as proposed, since there is no explicit polling. 174 | 175 | There is a bit of a foot-gun here because reference implementations make it possible to have multiple simultaneous readers or writers. However, that is somewhat difficult to do unintentionally, is already possible with the sync traits, and is possible even without the reference impls by cloning the underlying handles. 176 | 177 | The readiness mechanism using the `Ready` trait is extensible to other readiness notifications in the future. However, this is somewhat complicated since these notifications are nearly all platform-specific. (HUP is not, but requires special handling). 178 | 179 | ## Tweaks 180 | 181 | ### Elide the `non_blocking_` methods 182 | 183 | In this alternative, we'd keep the `Ready` trait, but the `Read` and `Write` traits would only have the async methods, not the `non_blocking_` ones. In the common case, the async read would return immediately and the user would not need to handle 'would block' errors. However, since in some cases the user would need to wait for the method to return, one could not share a single buffer between all reads on a thread. Furthermore, these functions couldn't be called from polling functions, or other non-async contexts. 184 | 185 | An alternative alternative would be to use the synchronous version of the `Read::read`, rather than `async::Read::non_blocking_read`. That has the right async-ness, but would need to handle 'would block' errors differently, since we would lose asynchronous-ness if we blocked on the `read` call. I don't think that can be done at the moment. It's possible we could do that with some form of async overloading, if we could have sync, async, and non-blocking versions of the same method (though note that that is an extension to the usual proposal for async overloading). 186 | 187 | ### `read_ready` and `write_ready` methods rather than a `Ready` trait 188 | 189 | This would lead to fewer traits and therefore a simpler API. However, there would be more methods overall (which would lead to code duplication for implementers). The mechanism would not be extensible to other readiness notifications, and it means that a single thread can't concurrently wait for both read and write readiness. 190 | 191 | ### Make `Ready` optional, rather than a super-trait 192 | 193 | This lets implementers choose if they want to support the memory-optimal path or just the ergonomic path. However, it also means that in generic code there is no way to use the memory-optimal path unless it is explicitly declared (i.e., changing the implementation becomes a breaking change, or code is reliant on intermediate generic code to require the `Ready` bound as well as `Read` or `Write`). 194 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /io-traits/README.md: -------------------------------------------------------------------------------- 1 | # Design of async IO traits 2 | 3 | `Read`, `Write`, `Seek`, `BufRead`, ... 4 | 5 | See also [issue 5](https://github.com/nrc/portable-interoperable/issues/5). 6 | 7 | ## Blog posts 8 | 9 | * [Async Read and Write traits](https://ncameron.org/blog/async-read-and-write-traits/) 10 | * [Async IO with completion-model IO systems](https://ncameron.org/blog/async-io-with-completion-model-io-systems/) 11 | 12 | ## Requirements 13 | 14 | Not in priority order. 15 | 16 | * The traits should be ergonomic to implement and to use 17 | - The async traits should be similar to the sync traits, ideally, the two sets of traits should be symmetric. Every concept from the sync versions should be expressible in the async versions. 18 | * The traits support the same variations as the sync traits 19 | - The traits must support vectored reads and writes 20 | - The traits must support reading into uninitialized memory 21 | * Generic usage 22 | - The traits must support concurrent reading and writing of a single resource 23 | - The traits should work well as trait objects (we're assuming the `dyn*` work will progress as expected) 24 | * The traits must work performantly with both readiness- (e.g., epoll) and completion-based systems (e.g., io_uring, IOCP) 25 | - When working with completion-based systems, the traits should support zero-copy reads and writes (i.e., the OS can read data directly into the user's buffer) 26 | - When working with readiness-based systems, the traits should not require access to buffers until IO is ready 27 | * The traits should permit maximum flexibility of buffers 28 | - buffers should not be constrained to a single concrete type (i.e., users can use there own buffer types with the IO traits and are not constrained to using a single concrete type such as `Vec`). 29 | - We should support buffers that are allocated on the stack or owned by other data structures 30 | * The traits should work in no_std scenarios 31 | 32 | ## Context: completion and readiness 33 | 34 | For efficient performance and memory usage, we must understand how async IO is performed at the OS level. There are multiple mechanisms across different OSes, but they can be broadly categorised into following either a readiness or completion model. 35 | 36 | The obvious starting place for the async IO traits is to simply add `async` to each method which performs IO. E.g., `fn read(&mut self, buf: &mut [u8]) -> Result` becomes `async fn read(&mut self, buf: &mut [u8]) -> Result`. This is very easy for the user because it abstracts all the details of when the task waits for the OS and how the OS communicates status of the operation to the IO library. However, this abstraction has some costs... 37 | 38 | ### Readiness 39 | 40 | Readiness-model IO is currently the most well-supported in async Rust. It is the model used by epoll (Linux) among others. 41 | 42 | From the perspective of the IO library, readiness IO has the following steps (I use read as an example, other operations are similar): 43 | 44 | * The library initiates the read. 45 | * The OS immediately returns and the library can schedule other work. 46 | * Later, the OS notifies the library that the read is ready. 47 | * The library passes a buffer to the OS to read into. The OS reads into the buffer and returns the bytes read, or returns an error. This step will never block. 48 | * The library may need to retry if the read failed, in particular if the OS gave an `EWOULDBLOCK` error indicating that no data was ready to read (i.e., the ready notification was a false positive). 49 | 50 | A strong advantage of this model is that the OS does not keep a buffer while waiting for IO to complete. That means a buffer can be allocated just in time to be used, or can be shared by multiple tasks (or the user can use many other memory handling strategies). This is important in the implementation of network servers where there may be many concurrent connections, the wait for IO can be long (since the wait is for a remote client), and the buffer must be fairly large since the size of the read is not known in advance. 51 | 52 | To implement readiness IO using the naive async read method described above, `read` takes the buffer reference. The IO library initiates the read with the OS and then waits to be scheduled; it must hold the buffer reference during this time. When the OS is ready, the library passes the buffer reference to the OS to read into and will retry if necessary. Finally it returns to the caller of `read`. This makes for an attractively simple API - the user does not need to be concerned with readiness notifications or retries, etc., however, there is no opportunity for the user to pass in the buffer just in time. I.e., it must be pre-allocated, which loses the primary advantage of the readiness model. 53 | 54 | ### Completion 55 | 56 | Completion-model IO is less well supported in the async Rust ecosystem, though the rise of io_uring is changing that (e.g., [Glommio](https://github.com/DataDog/glommio) and [Tokio-uring](https://github.com/tokio-rs/tokio-uring)). It is the model used by IOCP (Windows) and io_uring (Linux). 57 | 58 | From the perspective of the IO library, completion IO has the following steps (again, I use read as an example): 59 | 60 | * The library initiates the read and passes a buffer to read into. 61 | * The OS returns and the library can schedule other work. 62 | * Later, the OS reads directly into the buffer. When reading is complete (or if there is an error), it notifies the user process. 63 | 64 | In terms of sequencing, this is much closer to the naive read method given above. The advantage of this model is that the OS can read directly into the user's buffer without using an intermediate, internal buffer. Furthermore, the user can pass a reference into a target data structure. So, the IO can be zero copy: data is read directly from a device into its final destination. 65 | 66 | Unfortunately, there is a problem mapping completion to the naive Rust method too: cancellation. If the user wants to cancel the read, then it can send a cancellation message to the OS. This message is also async, it returns immediately but completes some time later when the OS will notify the user process that the IO was cancelled (or that there was an error). It is possible that an IO completes before the cancellation is processed. 67 | 68 | Now consider the lifetime of the buffer. The user process passes a reference to a buffer to the OS. The user process must keep the buffer alive (and must ensure nothing is written to the buffer) until the OS is done with it, i.e., either the IO completes or the IO is cancelled and the cancellation completes. Note that even if the IO is cancelled the buffer must be kept alive until either the IO or cancellation completes, it cannot be destroyed immediately. 69 | 70 | This is problematic in the Rust async model. In Rust, a future can be cancelled at any time (cancellation is not blockable on progress of any underlying operation). When a future is cancelled, the buffer passed to the IO library in `read` may be destroyed. If the buffer has been passed to the OS, that violates the required guarantee that the buffer is preserved until the IO completes. Even if the IO is cancelled, the cancellation completes from Rust's perspective before it completes from the OS's perspective. I.e., cancellation is unsound. 71 | 72 | Solutions will be explored below, but if we must fit the naive `read` signature, then any solution must require the library to own buffers used for IO and then to copy the contents of its buffer into the one provided by the user. Obviously, this loses the zero-copy advantage of completion IO. 73 | 74 | ## `Read` proposal 75 | 76 | This proposal consists of straightforward async `Read` (and `Write`) traits which closely follow their non-async counterparts. We also have sub-traits which are specialized for readiness and completion IO: `ReadinessRead` and `OwnedRead`. There are 'downcasting' methods on `Read` to facilitate converting to these traits where possible. I expect that we would stabilise the `Read` trait well in advance of the specialized traits. 77 | 78 | The `Read` trait (compare to non-async [`Read`](https://doc.rust-lang.org/nightly/std/io/trait.Read.html)): 79 | 80 | ```rust 81 | pub trait Read { 82 | async fn read(&mut self, buf: &mut [u8]) -> Result; 83 | async fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> Result { ... } 84 | async fn read_buf(&mut self, buf: &mut BorrowedCursor<'_>) -> Result<()> { ... } 85 | async fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> { ... } 86 | async fn read_buf_exact(&mut self, buf: &mut BorrowedCursor<'_>) -> Result<()> { ... } 87 | async fn read_buf_vectored(&mut self, bufs: &mut BorrowedSliceCursor<'_>) -> Result { ... } 88 | async fn read_to_end(&mut self, buf: &mut Vec) -> Result { ... } 89 | async fn read_to_string(&mut self, buf: &mut String) -> Result { ... } 90 | 91 | fn is_read_vectored(&self) -> bool { false } 92 | 93 | fn by_ref(&mut self) -> &mut Self 94 | where 95 | Self: Sized, 96 | { ... } 97 | 98 | fn bytes(self) -> Bytes 99 | where 100 | Self: Sized, 101 | { ... } 102 | 103 | fn chain(self, next: R) -> Chain 104 | where 105 | Self: Sized, 106 | { ... } 107 | 108 | fn take(self, limit: u64) -> Take 109 | where 110 | Self: Sized, 111 | { ... } 112 | 113 | fn as_ready(&mut self) -> Option<&mut impl ReadyRead> { 114 | None 115 | } 116 | 117 | fn as_owned(&mut self) -> Option<&mut impl OwnedRead> { 118 | None 119 | } 120 | 121 | fn as_ready_dyn(&mut self) -> Option<&mut dyn ReadyRead> { 122 | None 123 | } 124 | 125 | fn as_owned_dyn(&mut self) -> Option<&mut dyn OwnedRead> { 126 | None 127 | } 128 | } 129 | ``` 130 | 131 | Notes: 132 | 133 | * I have included `read_buf_vectored` based on an [open proposal](https://github.com/rust-lang/libs-team/issues/104). 134 | * `bytes`, `chain`, and `take` are not async methods but return types which implement `AsyncIterator` (c.f., `Iterator` in non-async `Read`). 135 | * The `as_*` methods are for downcasting. I have included both RPITIT and trait object versions, I'm not 100% sure that is necessary. These are the only methods without equivalents in non-async `Read`. 136 | 137 | The expectation is that nearly all code will use the `Read` trait only. Libraries which provide types which implement `Read`, should also provide implementations for `OwnedRead` and/or `ReadyRead` where possible. Users of these traits which support such methods can try downcasting before falling back to using plain `Read::read`. Adapter types (those which have an inner type which is a generic type bounded by `Read` and implement `Read` themselves) should implement `OwnedRead` and `ReadyRead` where their inner type does. 138 | 139 | ## Readiness read 140 | 141 | This trait is specialized for readiness IO systems such as epoll. 142 | 143 | ```rust 144 | pub trait Ready { 145 | async fn ready(&mut self, interest: Interest) -> Result; 146 | } 147 | 148 | // TODO do we need a trait at all here? 149 | // TODO does this work? 150 | pub trait ReadyRead: Ready + Read + std::io::Read { 151 | // fn non_blocking_read(&mut self, buf: &mut BorrowedCursor<'_>) -> Result>; 152 | // fn non_blocking_read_vectored(&mut self, bufs: &mut BorrowedSliceCursor<'_>) -> Result> { ... } 153 | } 154 | 155 | /// Express which notifications the user is interested in receiving. 156 | #[derive(Copy, Clone)] 157 | pub struct Interest(u32); 158 | 159 | /// Describes which operations are ready for an IO resource. 160 | #[derive(Copy, Clone)] 161 | pub struct Readiness(u32); 162 | 163 | /// Whether an IO operation is ready for reading/writing or would block. 164 | #[derive(Copy, Clone, Debug)] 165 | pub enum NonBlocking { 166 | Ready(T), 167 | WouldBlock, 168 | } 169 | 170 | impl Interest { 171 | pub const READ = ...; 172 | pub const WRITE = ...; 173 | pub const READ_WRITE = Interest(Interest::READ.0 | Interest::WRITE.0); 174 | } 175 | 176 | impl Debug for Interest { ... } 177 | 178 | impl Readiness { 179 | /// The resource is ready to read from. 180 | pub fn read(self) -> bool { ... } 181 | 182 | /// The resource is ready to write to. 183 | pub fn write(self) -> bool { ... } 184 | 185 | /// The resource has hung up. 186 | /// 187 | /// Note there may still be data to read. 188 | /// Note that the user does not *need* to check this method, even if the resource has hung up, 189 | /// the behaviour of `non_blocking_read` and `non_blocking_write` is defined and they should not 190 | /// panic. 191 | /// Note that the user does not need to request an interest in hup notifications, they may always 192 | /// be returned 193 | pub fn hup(self) -> bool { ... } 194 | } 195 | 196 | impl Debug for Readiness { ... } 197 | ``` 198 | 199 | Notes: 200 | 201 | * This approach requires a different idiom for reading, see below. That pattern facilitates avoiding memory allocation before data is ready to read. 202 | * To actually read data, use the synchronous read methods (thus the synchronous `Read` bound on `ReadyRead`). The underlying resource must have been opened in 'async mode' and coded appropriately so that if the operating system returns a 'would block' error, then this is surfaced to the caller, rather than blocking internally (i.e., the reading methods are implemented in a non-blocking fashion). 203 | 204 | 205 | ## Owned read 206 | 207 | `OwnedRead` is a specialized trait useful for completion IO systems, e.g., io_uring or IOCP. It circumvents the cancellation problem by letting the IO library own the buffer for the duration of the read. 208 | 209 | ```rust 210 | pub trait OwnedRead: Read { 211 | async fn read(&mut self, buf: OwnedBuf) -> (OwnedBuf, Result<()>); 212 | async fn read_exact(&mut self, buf: OwnedBuf) -> (OwnedBuf, Result<()>) { ... } 213 | async fn read_to_end(&mut self, buf: Vec) -> (Vec, Result) { ... } 214 | } 215 | ``` 216 | 217 | Notes: 218 | 219 | * Vectored reads are left as future work. 220 | * OwnedBuf permits reads into uninitialized memory, so there is no need for `read_buf` methods. 221 | * We must return the buffer since it is moved into the read methods. 222 | * `read_to_end` takes a `Vec` since it must extend the buffer and `OwnedBuf` is intentionally not extensible. 223 | * FIXME (open question): the methods take an OwnedBuf with the default allocator. If we permit any allocator then we require a generic method and `OwnedRead` cannot be used as a trait object. I'm not sure what is the right solution here. It's possible `dyn*` helps here, then we can take the allocator as a `dyn*` trait object (which for the common case of a zero or pointer-sized allocator, would not have to allocate). 224 | 225 | `OwnedBuf` is a new type similar in API to [`BorrowedBuf`](https://doc.rust-lang.org/nightly/std/io/struct.BorrowedBuf.html), but which owns its data. Since the buffer must be owned by the IO library during the read, it is passed as a buf rather than a cursor; the cursor is still used for writing. We use a concrete type rather than a trait to avoid generic methods or trait objects in `OwnedRead`. The idea is that any contiguous, owned buffer can be represented as an `OwnedBuf` and can be converted to or from the original type. There is provided support for easily converting to and from `Vec` and `Vec>`. 226 | 227 | ```rust 228 | pub struct OwnedBuf { 229 | data: *mut MaybeUninit, 230 | dtor: &'static dyn Fn(&mut OwnedBuf), 231 | capacity: usize, 232 | /// The length of `self.buf` which is known to be filled. 233 | filled: usize, 234 | /// The length of `self.buf` which is known to be initialized. 235 | init: usize, 236 | allocator: A, 237 | } 238 | 239 | impl OwnedBuf { 240 | #[inline] 241 | pub fn new(data: *mut MaybeUninit, dtor: &'static dyn Fn(&mut OwnedBuf), capacity: usize, filled: usize, init: usize) -> OwnedBuf { 242 | OwnedBuf::new_in(data, dtor, capacity, filled, init, Global) 243 | } 244 | 245 | #[inline] 246 | pub fn new_in(data: *mut MaybeUninit, dtor: &'static dyn Fn(&mut OwnedBuf), capacity: usize, filled: usize, init: usize, allocator: A) -> OwnedBuf { 247 | OwnedBuf { 248 | data, 249 | dtor, 250 | capacity, 251 | filled, 252 | init, 253 | allocator, 254 | } 255 | } 256 | 257 | /// SAFETY: only safe if self was created from a Vec or Vec, A>. 258 | #[inline] 259 | pub unsafe fn into_vec(self) -> Vec { 260 | let this = ManuallyDrop::new(self); 261 | Vec::from_raw_parts_in(this.data as *mut u8, this.filled, this.capacity, unsafe { ptr::read(&this.allocator) }) 262 | } 263 | 264 | /// SAFETY: only safe if self was created from a Vec or Vec, A>. 265 | #[inline] 266 | pub unsafe fn into_uninit_vec(self) -> Vec, A> { 267 | let this = ManuallyDrop::new(self); 268 | Vec::from_raw_parts_in(this.data, this.filled, this.capacity, unsafe { ptr::read(&this.allocator) }) 269 | } 270 | 271 | /// Returns the length of the initialized part of the buffer. 272 | #[inline] 273 | pub fn init_len(&self) -> usize { 274 | self.init 275 | } 276 | 277 | /// Returns a shared reference to the filled portion of the buffer. 278 | #[inline] 279 | pub fn filled(&self) -> &[u8] { 280 | // SAFETY: We only slice the filled part of the buffer, which is always valid 281 | unsafe { MaybeUninit::slice_assume_init_ref(slice::from_raw_parts(self.data, self.filled)) } 282 | } 283 | 284 | /// Returns a cursor over the unfilled part of the buffer. 285 | #[inline] 286 | pub fn unfilled(&mut self) -> OwnedCursor<'_, A> { 287 | OwnedCursor { 288 | start: self.filled, 289 | buf: self, 290 | } 291 | } 292 | 293 | /// Clears the buffer, resetting the filled region to empty. 294 | /// 295 | /// The number of initialized bytes is not changed, and the contents of the buffer are not modified. 296 | #[inline] 297 | pub fn clear(&mut self) -> &mut Self { 298 | self.filled = 0; 299 | self 300 | } 301 | 302 | /// Asserts that the first `n` bytes of the buffer are initialized. 303 | /// 304 | /// `OwnedBuf` assumes that bytes are never de-initialized, so this method does nothing when called with fewer 305 | /// bytes than are already known to be initialized. 306 | /// 307 | /// # Safety 308 | /// 309 | /// The caller must ensure that the first `n` unfilled bytes of the buffer have already been initialized. 310 | #[inline] 311 | pub unsafe fn set_init(&mut self, n: usize) -> &mut Self { 312 | self.init = max(self.init, n); 313 | self 314 | } 315 | } 316 | 317 | impl Drop for OwnedBuf { 318 | fn drop(&mut self) { 319 | (self.dtor)(self) 320 | } 321 | } 322 | 323 | pub struct OwnedCursor<'buf, A: 'static + Allocator> { 324 | buf: &'buf mut OwnedBuf, 325 | start: usize, 326 | } 327 | 328 | impl<'a, A: 'static + Allocator> OwnedCursor<'a, A> { 329 | /// Reborrow this cursor by cloning it with a smaller lifetime. 330 | /// 331 | /// Since a cursor maintains unique access to its underlying buffer, the borrowed cursor is 332 | /// not accessible while the new cursor exists. 333 | #[inline] 334 | pub fn reborrow<'this>(&'this mut self) -> OwnedCursor<'this, A> { 335 | OwnedCursor { 336 | buf: self.buf, 337 | start: self.start, 338 | } 339 | } 340 | 341 | /// Returns the available space in the cursor. 342 | #[inline] 343 | pub fn capacity(&self) -> usize { 344 | self.buf.capacity - self.buf.filled 345 | } 346 | 347 | /// Returns the number of bytes written to this cursor since it was created from a `BorrowedBuf`. 348 | /// 349 | /// Note that if this cursor is a reborrowed clone of another, then the count returned is the 350 | /// count written via either cursor, not the count since the cursor was reborrowed. 351 | #[inline] 352 | pub fn written(&self) -> usize { 353 | self.buf.filled - self.start 354 | } 355 | 356 | /// Returns a shared reference to the initialized portion of the cursor. 357 | #[inline] 358 | pub fn init_ref(&self) -> &[u8] { 359 | let filled = self.buf.filled; 360 | // SAFETY: We only slice the initialized part of the buffer, which is always valid 361 | unsafe { MaybeUninit::slice_assume_init_ref(&slice::from_raw_parts(self.buf.data, self.buf.init)[filled..]) } 362 | } 363 | 364 | /// Returns a mutable reference to the initialized portion of the cursor. 365 | #[inline] 366 | pub fn init_mut(&mut self) -> &mut [u8] { 367 | let filled = self.buf.filled; 368 | let init = self.buf.init; 369 | // SAFETY: We only slice the initialized part of the buffer, which is always valid 370 | unsafe { 371 | MaybeUninit::slice_assume_init_mut(&mut self.buf_as_slice()[filled..init]) 372 | } 373 | } 374 | 375 | /// Returns a mutable reference to the uninitialized part of the cursor. 376 | /// 377 | /// It is safe to uninitialize any of these bytes. 378 | #[inline] 379 | pub fn uninit_mut(&mut self) -> &mut [MaybeUninit] { 380 | let init = self.buf.init; 381 | unsafe { &mut self.buf_as_slice()[init..] } 382 | } 383 | 384 | /// Returns a mutable reference to the whole cursor. 385 | /// 386 | /// # Safety 387 | /// 388 | /// The caller must not uninitialize any bytes in the initialized portion of the cursor. 389 | #[inline] 390 | pub unsafe fn as_mut(&mut self) -> &mut [MaybeUninit] { 391 | let filled = self.buf.filled; 392 | &mut self.buf_as_slice()[filled..] 393 | } 394 | 395 | #[inline] 396 | unsafe fn buf_as_slice(&mut self) -> &mut [MaybeUninit] { 397 | slice::from_raw_parts_mut(self.buf.data, self.buf.capacity) 398 | } 399 | 400 | /// Advance the cursor by asserting that `n` bytes have been filled. 401 | /// 402 | /// After advancing, the `n` bytes are no longer accessible via the cursor and can only be 403 | /// accessed via the underlying buffer. I.e., the buffer's filled portion grows by `n` elements 404 | /// and its unfilled portion (and the capacity of this cursor) shrinks by `n` elements. 405 | /// 406 | /// # Safety 407 | /// 408 | /// The caller must ensure that the first `n` bytes of the cursor have been properly 409 | /// initialised. 410 | #[inline] 411 | pub unsafe fn advance(&mut self, n: usize) -> &mut Self { 412 | self.buf.filled += n; 413 | self.buf.init = max(self.buf.init, self.buf.filled); 414 | self 415 | } 416 | 417 | /// Initializes all bytes in the cursor. 418 | #[inline] 419 | pub fn ensure_init(&mut self) -> &mut Self { 420 | for byte in self.uninit_mut() { 421 | byte.write(0); 422 | } 423 | self.buf.init = self.buf.capacity; 424 | 425 | self 426 | } 427 | 428 | /// Asserts that the first `n` unfilled bytes of the cursor are initialized. 429 | /// 430 | /// `BorrowedBuf` assumes that bytes are never de-initialized, so this method does nothing when 431 | /// called with fewer bytes than are already known to be initialized. 432 | /// 433 | /// # Safety 434 | /// 435 | /// The caller must ensure that the first `n` bytes of the buffer have already been initialized. 436 | #[inline] 437 | pub unsafe fn set_init(&mut self, n: usize) -> &mut Self { 438 | self.buf.init = max(self.buf.init, self.buf.filled + n); 439 | self 440 | } 441 | 442 | /// Appends data to the cursor, advancing position within its buffer. 443 | /// 444 | /// # Panics 445 | /// 446 | /// Panics if `self.capacity()` is less than `buf.len()`. 447 | #[inline] 448 | pub fn append(&mut self, buf: &[u8]) { 449 | assert!(self.capacity() >= buf.len()); 450 | 451 | // SAFETY: we do not de-initialize any of the elements of the slice 452 | unsafe { 453 | MaybeUninit::write_slice(&mut self.as_mut()[..buf.len()], buf); 454 | } 455 | 456 | // SAFETY: We just added the entire contents of buf to the filled section. 457 | unsafe { 458 | self.set_init(buf.len()); 459 | } 460 | self.buf.filled += buf.len(); 461 | } 462 | } 463 | 464 | impl<'a, A: 'static + Allocator> Write for OwnedCursor<'a, A> { 465 | fn write(&mut self, buf: &[u8]) -> Result { 466 | self.append(buf); 467 | Ok(buf.len()) 468 | } 469 | 470 | fn flush(&mut self) -> Result<()> { 471 | Ok(()) 472 | } 473 | } 474 | 475 | fn drop_vec(buf: &mut OwnedBuf) { 476 | let buf = ManuallyDrop::new(buf); 477 | let _vec = unsafe { Vec::from_raw_parts_in(buf.data, buf.filled, buf.capacity, ptr::read(&buf.allocator)) }; 478 | } 479 | 480 | impl From, A>> for OwnedBuf { 481 | fn from(v: Vec, A>) -> OwnedBuf { 482 | let (data, len, capacity, allocator) = v.into_raw_parts_with_alloc(); 483 | OwnedBuf { 484 | data, 485 | dtor: &drop_vec, 486 | capacity, 487 | filled: len, 488 | init: len, 489 | allocator, 490 | } 491 | } 492 | } 493 | 494 | impl From> for OwnedBuf { 495 | fn from(v: Vec) -> OwnedBuf { 496 | let (data, len, capacity, allocator) = v.into_raw_parts_with_alloc(); 497 | OwnedBuf { 498 | data: data as *mut MaybeUninit, 499 | dtor: &drop_vec, 500 | capacity, 501 | filled: len, 502 | init: len, 503 | allocator, 504 | } 505 | } 506 | } 507 | ``` 508 | 509 | ## Usage 510 | 511 | A simple read looks like: 512 | 513 | ```rust 514 | async fn read_example(reader: &mut impl Read) -> Result<()> { 515 | let mut buf = [0; 1024]; 516 | reader.read(&mut buf).await?; 517 | // Use buf 518 | Ok(()) 519 | } 520 | ``` 521 | 522 | The specialized traits can be used by themselves: 523 | 524 | ```rust 525 | async fn read_example_owned(reader: &mut impl OwnedRead) -> Result<()> { 526 | let mut buf = vec![0; 1024]; 527 | let result = reader.read(buf).await; 528 | buf = result.0; 529 | result.1?; 530 | // Use buf 531 | Ok(()) 532 | } 533 | 534 | async fn read_example_ready(reader: &mut impl ReadyRead) -> Result<()> { 535 | loop { 536 | reader.ready(Interest::READABLE).await?; 537 | 538 | let mut buf = [0; 1024]; 539 | if let NonBlocking::Ready(n) = std::io::Read::read(reader, buf)? { 540 | // Use buf 541 | return Ok(()); 542 | } 543 | } 544 | } 545 | ``` 546 | 547 | Where a user of read wants optimal performance and cannot know if the reader implements a specialized trait, then it can test by downcasting. I demonstrate testing for both traits; most end user code would only test for the trait that maximizes their performance. 548 | 549 | ```rust 550 | async fn read_example(reader: &mut impl Read) -> Result<()> { 551 | if let Some(reader) = reader.as_owned() { 552 | let mut buf = vec![0; 1024]; 553 | let result = reader.read(buf).await; 554 | buf = result.0; 555 | result.1?; 556 | // Use buf 557 | } else if let Some(reader) = reader.as_ready() { 558 | loop { 559 | reader.ready(Interest::READABLE).await?; 560 | 561 | let mut buf = [0; 1024]; 562 | if let NonBlocking::Ready(n) = std::io::Read::read(reader, buf)? { 563 | // Use buf 564 | return Ok(()); 565 | } 566 | } 567 | } else { 568 | let mut buf = [0; 1024]; 569 | reader.read(&mut buf).await?; 570 | // Use buf 571 | } 572 | Ok(()) 573 | } 574 | ``` 575 | 576 | Wrapper traits should implement all traits by dispatching to their inner traits (and doing whatever processing is necessary for the specifics of the trait). The `Read` implementation should not downcast. 577 | 578 | ## `Write` proposal 579 | 580 | `Write` follows `Read` in having a simple trait which is a straightforward async version of the existing sync `Write` trait, and having specialized `ReadyWrite` and `OwnedWrite` traits for optimal performance on readiness and completion systems, respectively. 581 | 582 | ```rust 583 | pub trait Write { 584 | async fn write(&mut self, buf: &[u8]) -> Result; 585 | async fn flush(&mut self) -> Result<()>; 586 | async fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> Result { ... } 587 | fn is_write_vectored(&self) -> bool { ... } 588 | async fn write_all(&mut self, buf: &[u8]) -> Result<()> { ... } 589 | async fn write_all_vectored(&mut self, bufs: &mut [IoSlice<'_>]) -> Result<()> { ... } 590 | async fn write_fmt(&mut self, fmt: Arguments<'_>) -> Result<()> { ... } 591 | 592 | fn by_ref(&mut self) -> &mut Self where Self: Sized { ... } 593 | } 594 | 595 | // TODO does it work? Should we have a trait? 596 | pub trait ReadyWrite: Ready + Write + std::io::Write {} 597 | 598 | // Used for completion model systems. 599 | pub trait OwnedWrite: Write { 600 | async fn write(&mut self, buf: OwnedBuf) -> (OwnedBuf, Result<()>); 601 | async fn write_all(&mut self, buf: OwnedBuf) -> (OwnedBuf, Result<()>) { ... } 602 | } 603 | ``` 604 | 605 | Notes: 606 | 607 | * vectored writes in `OwnedWrite` are left as future work. 608 | * `OwnedWrite` functions take `OwnedBuf`, not `OwnedCursor` despite an implicit contract that they should not mutate the buffer. This is because ownership must be transferred and `OwnedCursor` borrows from the underlying buffer. Perhaps this indicates poor naming? Perhaps we should have a read-only but owned view of the buffer? This is tricky because once it is returned to the caller, they should be able to mutate it, so this really requires something which doesn't fit with Rust's usual 'ownership implies mutability' principal. 609 | * See the notes on `ReadyRead`, similar things apply to `ReadyWrite`. 610 | 611 | 612 | ## `BufRead` proposal 613 | 614 | ```rust 615 | pub trait BufRead: Read { 616 | async fn fill_buf(&mut self) -> Result<&[u8]>; 617 | fn consume(&mut self, amt: usize); 618 | 619 | async fn read_until(&mut self, byte: u8, buf: &mut Vec) -> Result { ... } 620 | async fn read_line(&mut self, buf: &mut String) -> Result { ... } 621 | #[unstable] 622 | async fn has_data_left(&mut self) -> Result { ... } 623 | } 624 | ``` 625 | 626 | * `fill_buf` and `consume` are required methods. `consume` does not need to be async since it is just about buffer management, no IO will take place. 627 | * `has_data_left` must be async since it might fill the buffer to answer the question (it is currently unstable in `BufRead` which is why I've added that annotation). 628 | * I've elided `split` and `lines` methods since these are async iterators and there are still open questions there. I assume we will add these later. Besides the questions about async iterators, I don't think there is anything too interesting about these methods. 629 | 630 | 631 | ### `BufReader` 632 | 633 | `BufReader` is a concrete type, it's a utility type for converting objects which implement `Read` into objects which implement `BufRead`. I.e., it encapsulates a reader with a buffer to make a reader with an internal buffer. `BufReader` provides its own buffer and does not let the user customise it. 634 | 635 | I think that we don't need a separate `async::BufReader` type, but rather we need to duplicate the `impl BufReader` impl for `R: async::Read` and to implement `async::BufRead` where `R: async::Read`. Similarly, we would duplicate the `impl BufReader` impl as `impl BufReader` to provide an async versions of `seek_relative`. This might be an area where async overloading is useful. 636 | 637 | 638 | ## Seek 639 | 640 | ```rust 641 | pub trait Seek { 642 | async fn seek(&mut self, pos: SeekFrom) -> Result; 643 | 644 | async fn rewind(&mut self) -> Result<()> { ... } 645 | async fn stream_len(&mut self) -> Result { ... } 646 | async fn stream_position(&mut self) -> Result { ... } 647 | } 648 | ``` 649 | 650 | The async `Seek` trait is a simple `async`-ification of the sync trait. 651 | 652 | The `Ready` trait could be extended to support seeking, but I don't think that is necessary. Seek is only useful with buffered readers, files, and similar. In these cases, the memory advantages of using `Ready` are diminished. 653 | 654 | There was some discussion about `Seek` in Tokio. One of the key sticking points which led to their `start_seek`/`seek_complete` API was that a future should not have any observable side effects until it is ready, and `poll_seek` method does not satisfy that invariant (since the state of a file might be changed by a seek that did not complete before the seek was cancelled). I believe this is not an issue for async methods, since there can be no assumption of side-effect freedom because polling is encapsulated. 655 | 656 | ### Extension: `read_at`/`write_at` 657 | 658 | `read_at`/`write_at` is arguably a better API than using `seek` and read/write, especially in async programming, because the operation is atomic and therefore not susceptible to race condition errors. However, we should still have an `async::Seek` trait for symmetry with the sync trait, so `read_at`/`write_at` is an extension rather than an alternative. 659 | 660 | 661 | ## Alternatives 662 | 663 | There are several alternatives to the design proposed above. The first few are presented in separate files and I consider them feasible (though inferior to the above proposal), the later few are clearly sub-optimal and I haven't described them in depth. 664 | 665 | ### Readiness super trait 666 | 667 | [super-ready.md](alternatives/super-ready.md) 668 | 669 | ### Async traits 670 | 671 | [async-traits.md](alternatives/async-traits.md) 672 | 673 | ### A Split trait 674 | 675 | [split-trait.md](alternatives/split-trait.md) 676 | 677 | ### Tweak: non-blocking read/write methods 678 | 679 | Previously, the readiness traits in different proposals had required and provided methods for non-blocking reads/write. We don't think these are necessary because the underlying resources can be opened in non-blocking mode and then the read/write methods of the synchronous `Read`/`Write` traits can be used. H/T TODO cite Yosh's blog post. 680 | 681 | ### Tweak: make vectored IO a separate trait 682 | 683 | [tweaks.md](alternatives/tweaks.md) 684 | 685 | ### Tweak: only provide ReadBuf methods 686 | 687 | [tweaks.md](alternatives/tweaks.md) 688 | 689 | ### Tweak: no impls on reference types 690 | 691 | [tweaks.md](alternatives/tweaks.md) 692 | 693 | ### Polling read/write methods 694 | 695 | We could continue to use `poll_read` and `poll_write` instead of the `non_blocking_` methods. This would allow using trait objects without allocating and can support simultaneous read/write. However, this is much less ergonomic than this proposal and doesn't permit impls on reference types. 696 | 697 | For the sake of having some code to look at, here is the current `Read` trait from futures.rs: 698 | 699 | ```rust 700 | pub trait Read { 701 | fn poll_read( 702 | self: Pin<&mut Self>, 703 | cx: &mut Context<'_>, 704 | buf: &mut [u8], 705 | ) -> Poll>; 706 | 707 | fn poll_read_vectored( 708 | self: Pin<&mut Self>, 709 | cx: &mut Context<'_>, 710 | bufs: &mut [IoSliceMut<'_>], 711 | ) -> Poll> { ... } 712 | } 713 | ``` 714 | 715 | Given how un-ergonomic this approach is, I don't think it is worth pursuing unless other approaches turn out to be dead ends. (We could add the async methods as provided methods to make this approach more ergonomic for users, however, it is still less ergonomic for implementers and there is no real benefit other than backwards compatibility). 716 | 717 | ### Offer only `BufRead`, and not `Read` (likewise for `Write`) 718 | 719 | It might be possible to reduce the set of IO traits to only the buffered varieties by using a sophisticated buffer or buffer manager type to abstract over the various ways in which buffers can be managed and passed to read methods. By having the buffer manager supply the buffer, there is no need to wait on readiness and instead the buffer manager creates or provides the buffer when the reader is ready. 720 | 721 | The approach would work well with completion based IO as well as readiness. However, this approach adds some complexity for the simple cases of read, would be a radical departure from the existing sync traits, and it's unclear if the lifetimes can be made to work in all cases. 722 | 723 | ### Add non-async version of readiness and owned read support 724 | 725 | Although using the readiness and owned APIs is more strongly motivated in async code, there is no reason it can't be used in non-async code. We might consider adding support for explicit readiness support to std. This would increase the symmetry between sync and async libraries at the expense of increasing the surface are of std. 726 | 727 | ## Requirements discussion 728 | 729 | ### The traits should be ergonomic to implement and to use 730 | 731 | On the plus side, the primary proposal is simple in the simple case (both to use and define) and is as close to symmetric with the non-async traits as possible (assuming we support specialized IO modes at all). It is always possible to use the specialized traits from the basic one, without requiring multiple versions of functions or data types. On the minus side, 'good citizen' libraries do have some extra work to do (mostly unavoidable if we are to support the specialized modes), in particular, optimally implementing a wrapper type requires some work and that work is not enforced or encouraged by the types (i.e., one can just write a naive `read` impl and there is no warning). 732 | 733 | The split trait and polling alternatives are more complex and less ergonomic. Implementing the IO traits in the primary proposal is more complex than in the simple async traits alternative, but that does not support optimal performance. All the other tweaks or minor alternatives make the primary proposal more complex or less symmetric. 734 | 735 | I believe the `OwnedRead` trait is the most ergonomic solution for efficient completion reads. It closely matches existing solutions from the community, and allows implementing other models (such as the library managing buffers) using `OwnedRead` as a building block. 736 | 737 | ### The traits support the same variations as the sync traits 738 | 739 | All alternatives support vectored reads and writes and reading into uninitialized memory, in a similar way to the sync traits. There is nothing specific to any alternative which makes this better or worse. 740 | 741 | We could remove support for `&mut [u8]` reads and only support `BorrowedBuf` reads. This would be a simpler API and just as flexible, at the cost of a tiny reduction in ergonomics and a reduction in symmetry. 742 | 743 | ### Generic usage 744 | 745 | The primary proposal supports concurrent reading and writing by implementing `Read` and `Write` for reference types in the same manner as for the sync traits. 746 | 747 | Other alternatives support concurrent reads and writes similarly, or by polling or using a 'split' trait. 748 | 749 | Sometimes it is necessary to permit concurrent reads and writes in generic code. In this case, the reference impls solution does not work: it requires `T: Read + Write` and we can't easily require reference impls for concurrent reads and writes. The primary proposal would work in this case via `ReadyRead`, but not in the simple or owned case (this is similar to the situation in non-async code). The `Split` trait also works here, using `T: Split` rather than `T: Read + Write`. 750 | 751 | The traits are usable as trait objects in all variations, assuming that we can support async trait objects in the language. 752 | 753 | By using `OwnedBuf` (a concret type), `OwnedRead` is also usable as a trait object (module trait object support for async methods). This would not be the case if using a trait for the buffer type. 754 | 755 | ### The traits must work performantly with both readiness- (e.g., epoll) and completion-based systems (e.g., io_uring, IOCP) 756 | 757 | The primary proposal addresses this requirement well by providing the specialized traits. The only downside is that there is no guarantee (or static checking) of whether the more performant modes are available. 758 | 759 | ### The traits should permit maximum flexibility of buffers 760 | 761 | We do not provide a trait where the IO library manages the buffers (in some previous proposals I called this `ManagedRead`). One could use `BufRead` for this, though the API is optimised for tasks which can't be done without buffering, rather than generic reading. One could also implement such a trait in a third-party crate on top of `OwnedRead`. An alternative would be to include such a trait in std. This could be done later. I opted not to for the sake of simplicity. 762 | 763 | `OwnedBuf` is designed to be efficiently interoperable with any reasonable buffer type. 764 | 765 | ### The traits should work in no_std scenarios 766 | 767 | Now that `std::Error` has moved to core, none of the alternatives present any further difficulty in working with no_std crates. 768 | --------------------------------------------------------------------------------