How does futures.rs work generically with different async runtimes then?
As far as async runtime fragmentation you can write code that is generic across runtimes, it's just less convenient to use. If your code only works with plain data and futures you're fine on any runtime, it's when you want to spawn new tasks or block on a task that you have to have a runtime dependency. Async vs sync is the classic "what color is your function" problem which green threads kind of solves but not completely so you still have to understand what is happening so you don't stall every task on a native thread with CPU heavy or blocking work.
This is true, but I think there are a few notable caveats:
1. Although you can't generically build libraries per runtime, it is possible to right a library supporting an explicit set of runtimes with some boilerplate; the simplest way is to just to abstract all of the async primitives you want into an API to use internally, use feature flags to implement them based on which runtime you want to support, and then only use that API in the rest of your library (which both avoids the need for conditional compilation outside of that one wrapper module for async stuff and reduces the surface of places you'd need to update to add or remove support for a runtime or if you want to update a runtime to a version that isn't backwards compatible.
2. Although there are quite a lot of runtimes that exist in the ecosystem right now (and more could enter the scene in the future), in practice the usage of a lot of these runtimes is quite small. For a few examples, at the time of my looking up, tokio has 66.7 million downloads overall and 10.3 million "recent" ones; async-std has 9 million overall and 1.3 million recent; smol has 1.6 million overall and 158k recent. There's diminishing returns for each new runtime you add support for, so while there's some up front burden in terms of supporting more than one runtime (like I describe above), the burden over time is not going to be super high.
3. Rust has a history of starting out by providing lower-level primitives for things, letting the community iterate over various ideas in the space for a few years, and then eventually settling on a single or small set of pseudo-official crates for them. I think the error handling APIs are the best example of this; Rust 1.0 launched with the standard library Error trait and the `try!` macro, and the community iterated over a bunch of potential solution (error-chain, failure, and probably a bunch of others I don't even remember at the moment), and for a few years it was a bit messy. Meanwhile, the standard library added the `?` operator and added a replacement for one of the `Error` trait methods (deprecating the old ones), and eventually the churn settled down and most people just use `thiserror` and `anyhow` now. There's still some lingering things to standardize like backtraces for errors, but overall the error handling space is way less fragmented now than basically at any point in the past. I'd argue that the async runtime churn is already starting to trend towards equilibrium; if this turns out to be the case, the costs of supporting multiple runtimes will continue to go down, and I'm guessing (hoping?) that within a few years people will have either mostly standardized on a single runtime or we'll have a standard solution for wrapping the small set of runtimes that retain any significant usage in the community.
2. Tokio has the best support right now but Tokio is very difficult to write libraries for and may not be the best designed async library. Monoio looks like a very good design and may be the best option for web servers because you don't have to deal with Send and Sync.
3. This method usually works but for async they should have been more opinionated. Stackfull coroutines line [May](https://github.com/Xudong-Huang/may) would have worked best for 99% of the use cases and maybe there could be a separate no-cost async setup for embedded. But trying to have both under the same async paradigm is causing huge issues.