_includes/
_layouts/
_pages/
_posts/
2010-04-24-staying-the-hell-out-of-insert-mode.md
4.2 KiB
2020-07-19-popol.md
9.1 KiB
2021-01-22-nakamoto.md
8.2 KiB
2024-01-31-segfault-the-mother-of-invention.md
6.2 KiB
assets/
css/
fonts/
images/
js/
.gitignore
62 B
Gemfile
68 B
Gemfile.lock
1.7 KiB
Makefile
198 B
README
192 B
_config.yml
278 B
avatar.png
44.3 KiB
cloudhead.gpg
892 B
favicon.png
179 B
index.md
79 B
publish
1.6 KiB
robots.txt
33 B
_posts/2020-07-19-popol.md
raw
| 1 | --- |
| 2 | title: "Popol: Minimal non-blocking I/O with Rust" |
| 3 | headline: "Popol: Minimal non-blocking I/O with Rust" |
| 4 | date: "19.07.2020" |
| 5 | teaser: "async i/o" |
| 6 | type: note |
| 7 | slug: popol |
| 8 | crate: popol |
| 9 | layout: default |
| 10 | --- |
| 11 | |
| 12 | Asynchronous I/O in Rust is still very much in its infancy. *Async/await*, |
| 13 | Rust's solution to the problem, was recently stabilized and so when came |
| 14 | the time to implement some peer-to-peer networking code, I reached for the shiny new |
| 15 | feature. To my dismay, it created more problems than it solved. Indeed, I quickly |
| 16 | regretted going down that path and searched for alternatives. All I was |
| 17 | looking for was an easy way to handle between fifty and a hundred TCP connections |
| 18 | (`net::TcpStream`) efficiently to implement the reactor for [nakamoto][], a |
| 19 | Bitcoin client I've been working on. |
| 20 | |
| 21 | The crux of the problem with *async/await* is that it is incompatible with the |
| 22 | standard library traits, such as `Read` and `Write`. Partly, this is due to the |
| 23 | design decisions behind Rust's async implementation: the choice of cooperative |
| 24 | multitasking instead of preemptive multitasking, for instance. |
| 25 | Languages built on the latter don't suffer from this scission. Good examples |
| 26 | include Haskell and Erlang, which don't have a language-level concept of "async" |
| 27 | -- the implementation is transparent to the user. |
| 28 | |
| 29 | As it stands, the *async/await* system in Rust comes with an incompatible set of |
| 30 | traits: `AsyncRead` and `AsyncWrite`, which don't play nicely with the standard |
| 31 | library. Channels, for example the amazing *crossbeam-channel* don't work in |
| 32 | asynchronous code -- you need an *async* variant. Alternatively, most of the |
| 33 | runtimes that effectively provide replacement types for channels, files and |
| 34 | sockets, such as *async-std* and *tokio*, have a large dependency footprint, |
| 35 | inherent in the complexity of the problem. For example, [smol][], one of the |
| 36 | "small" runtimes still consists of about 24 crates, including its transitive |
| 37 | dependencies, as of today. |
| 38 | |
| 39 | $ cargo tree -p smol -e no-dev --prefix none --no-dedupe | sort | uniq | wc -l |
| 40 | 24 |
| 41 | |
| 42 | Async/await may be a "zero-cost abstraction", but it certainly has a cost in |
| 43 | usability, maintenance and complexity. I think it still has a long way to go |
| 44 | before the benefits outweigh the costs. |
| 45 | |
| 46 | So what are the alternatives? Well, for parallelism, we have threads. Threads |
| 47 | are *great* in Rust. However, for peer-to-peer networking, threads can get a little |
| 48 | unwieldy. For my use case, the ability to handle between a dozen and a few |
| 49 | hundred open connections is all I need. And I believe this is true for *most* |
| 50 | people, especially in the peer-to-peer space. This requirement places us squarely |
| 51 | within the territory of the venerable `poll(2)` system call, which is available on |
| 52 | almost all platforms nowadays.[^1] |
| 53 | |
| 54 | [*Popol*][code] is designed as a minimal ergonomic wrapper around `poll`, |
| 55 | built for use cases such as peer-to-peer networking, where you typically have |
| 56 | no more than a few hundred concurrent connections. It's meant to be familiar |
| 57 | enough for those with experience using [mio][], but a little easier to use, and |
| 58 | a lot smaller.[^2] |
| 59 | |
| 60 | ```rust |
| 61 | // Create a registry to hold I/O sources. |
| 62 | let mut sources = popol::Sources::with_capacity(1); |
| 63 | // Create an events buffer to hold readiness events. |
| 64 | let mut events = popol::Events::with_capacity(1); |
| 65 | |
| 66 | // Register the program's standard input as a source of "read" readiness events. |
| 67 | // The first parameter is the key we want to associate with the source. Since |
| 68 | // we only have one source in this example, we just pass in the unit type. |
| 69 | sources.register((), &io::stdin(), popol::interest::READ); |
| 70 | |
| 71 | // Wait on our event sources for at most 6 seconds. If an event source is |
| 72 | // ready before then, process its events. Otherwise, timeout. |
| 73 | match sources.wait_timeout(&mut events, Duration::from_secs(6)) { |
| 74 | Ok(()) => {} |
| 75 | Err(err) if err.kind() == io::ErrorKind::TimedOut => process::exit(1), |
| 76 | Err(err) => return Err(err), |
| 77 | } |
| 78 | |
| 79 | // Iterate over source events. Since we only have one source |
| 80 | // registered, this will only iterate once. |
| 81 | for ((), event) in events.iter() { |
| 82 | // The standard input has data ready to be read. |
| 83 | if event.readable || event.hangup { |
| 84 | let mut buf = [0; 1024]; |
| 85 | |
| 86 | // Read what we can from standard input and echo it. |
| 87 | match io::stdin().read(&mut buf[..]) { |
| 88 | Ok(n) => io::stdout().write_all(&buf[..n])?, |
| 89 | Err(err) => panic!(err), |
| 90 | } |
| 91 | } |
| 92 | } |
| 93 | ``` |
| 94 | |
| 95 | By building on `poll`, we have the following benefits: |
| 96 | |
| 97 | * Much smaller implementation than even the smallest *async/await* runtimes.[^3] |
| 98 | * All the standard library traits just work (`io::Read`, `io::Write`, etc.) |
| 99 | * All the standard library sockets work, (`net:::TcpStream`, `net::UdpStream`, etc.) |
| 100 | * No "runtime" keeps the code much easier to reason about. |
| 101 | |
| 102 | Some people ask why I didn't build on `epoll`[^4]. There's a couple of reasons: |
| 103 | |
| 104 | 1. `epoll` is more complex than `poll` and thus requires us to write more code. |
| 105 | 2. `epoll` isn't portable; it only works on Linux. |
| 106 | 3. `epoll` requires a system call to add, remove or modify the list of sources |
| 107 | to poll. This introduces new failure modes. |
| 108 | 4. Even though `epoll` should be able to handle more connections, `poll` is |
| 109 | *plenty enough* for all the use-cases I'm interested in. |
| 110 | |
| 111 | How about *mio*? It solves the first two problems, one might say. But |
| 112 | there are a couple of things that irked me about *mio*. In particular: |
| 113 | |
| 114 | * *mio* comes bundled with its own socket types, which you are supposed to use |
| 115 | instead of the ones provided by the standard library. |
| 116 | * *mio* identifies event sources by a `Token` type, which is basically a wrapper |
| 117 | around a `u64`. This is leaked from the underlying `epoll` implementation, |
| 118 | and is less than ideal. |
| 119 | |
| 120 | Compared to *mio*, *popol*: |
| 121 | |
| 122 | * Is a lot smaller (about 10% of the size). |
| 123 | * Allows you to use any type implementing `Eq` and `Clone` to identify event |
| 124 | sources. It's not limited to `u64`. |
| 125 | * Supports standard library sockets (and other file types) transparently. |
| 126 | * Supports multiple *wakers* per wait call. |
| 127 | * Has only a single API call that can fail, since it's built on `poll`. |
| 128 | |
| 129 | Some of the above is exemplified by popol's `Sources::register` function below: |
| 130 | |
| 131 | ```rust |
| 132 | impl<K: Eq + Clone> Sources<K> { |
| 133 | /// Register a new source with the given key, and wait for the specified |
| 134 | /// events. |
| 135 | pub fn register(&mut self, key: K, source: &impl AsRawFd, events: Interest); |
| 136 | } |
| 137 | ``` |
| 138 | |
| 139 | It goes without saying that *popol* is also a lot less mature than *mio*, |
| 140 | so use it at your own risk! *Popol* also doesn't currently support Windows, |
| 141 | though this is planned. |
| 142 | |
| 143 | --- |
| 144 | |
| 145 | Finally, we can look at an example of a TCP server which accepts incoming |
| 146 | connections and registers them with *popol*. This example showcases the |
| 147 | use of more complex types for `K`: |
| 148 | |
| 149 | ```rust |
| 150 | use std::{io, net}; |
| 151 | use popol; |
| 152 | |
| 153 | /// The identifier we'll use with `popol` to figure out the source of an event. |
| 154 | /// The `K` in `Sources<K>`. |
| 155 | #[derive(Eq, PartialEq, Clone)] |
| 156 | enum Source { |
| 157 | /// An event from a connected peer. |
| 158 | Peer(net::SocketAddr), |
| 159 | /// An event on the listening socket. Most probably a new peer connection. |
| 160 | Listener, |
| 161 | } |
| 162 | |
| 163 | let listener = net::TcpListener::bind("0.0.0.0:8888")?; |
| 164 | let mut sources = popol::Sources::new(); |
| 165 | let mut events = popol::Events::new(); |
| 166 | |
| 167 | // Register the listener socket, using the corresponding identifier. |
| 168 | sources.register(Source::Listener, &listener, popol::interest::READ); |
| 169 | |
| 170 | // It's important to set the socket in non-blocking mode. This allows |
| 171 | // us to know when to stop accepting connections. |
| 172 | listener.set_nonblocking(true)?; |
| 173 | |
| 174 | loop { |
| 175 | // Wait for something to happen on our sources. |
| 176 | sources.wait(&mut events)?; |
| 177 | |
| 178 | for (key, event) in events.iter() { |
| 179 | match key { |
| 180 | Source::Listener => loop { |
| 181 | // Accept as many connections as we can. |
| 182 | let (conn, addr) = match listener.accept() { |
| 183 | Ok((conn, addr)) => (conn, addr), |
| 184 | |
| 185 | Err(e) if e.kind() == io::ErrorKind::WouldBlock => break, |
| 186 | Err(e) => return Err(e), |
| 187 | }; |
| 188 | // Register the new peer using the `Peer` variant of `Source`. |
| 189 | sources.register( |
| 190 | Source::Peer(addr), |
| 191 | &conn, |
| 192 | popol::interest::ALL |
| 193 | ); |
| 194 | } |
| 195 | Source::Peer(addr) if event.readable => { |
| 196 | println!("{} has data to be read", addr); |
| 197 | } |
| 198 | Source::Peer(addr) if event.writable => { |
| 199 | println!("{} is ready to be written", addr); |
| 200 | } |
| 201 | } |
| 202 | } |
| 203 | } |
| 204 | ``` |
| 205 | |
| 206 | --- |
| 207 | |
| 208 | *Popol* is available on [crates.io][crate]. The source code is hosted on |
| 209 | [GitHub][code]. Discuss on [reddit][]. |
| 210 | |
| 211 | [^1]: Even Windows has an implementation of poll, called `WSAPoll`. |
| 212 | [^2]: The name "popol" comes from [Popol Vuh](https://en.wikipedia.org/wiki/Popol_Vuh). |
| 213 | [^3]: *Popol* is currently around 400 lines of code, excluding tests. The only dependency is the *libc* crate. |
| 214 | [^4]: <https://en.wikipedia.org/wiki/Epoll> |
| 215 | |
| 216 | [nakamoto]: https://github.com/cloudhead/nakamoto |
| 217 | [smol]: https://crates.io/crates/smol |
| 218 | [mio]: https://crates.io/crates/mio |
| 219 | [code]: https://github.com/cloudhead/popol |
| 220 | [crate]: https://crates.io/crates/popol |
| 221 | [docs]: https://docs.rs/popol |
| 222 | [reddit]: https://www.reddit.com/r/rust/comments/htyyf3/popol_minimal_nonblocking_io_with_rust/ |