Understanding Rust futures by going way too deep
fasterthanli.me
fasterthanli.me
In contrast to some other comments here, I love the narrative style. After all, these are half tutorials half entertainment - and the entertainment keeps it enticing. I would not have spent nearly two hours reading about the nitty-gritty of futures implementation details today if it weren't for the way you write about things.
To put it the other way - if I wanted to read a super condensed, concise reference of how these things work, I would, you know, read a reference. The narrative style of building things on your own, solving problems as you face them, picking tools and libraries on the way, feels relatable as the everyday lived experience of software development. It feels appropriate for the medium - it's a blog after all!
Besides, blogs are the perfect place to mention the tiny useful tools and libraries people might not have heard of. References and books might err on the side of being "neutral" and not making opinionated recommendations, but this can also lead to unknown unknowns. People don't know what they are missing out on! Luckily, blogs have more freedom here. Mentioning things like `cargo-edit`, `serde` and others can probably make some readers feel like "one of today's lucky ten thousand" - enjoying the newly-gained quality of life improvements.
The methods include strategically inserted panic!, some nice crates with examples how to use them... I cannot say I've learnt a lot about async, but I got few insights on technicalities of self-learning.
Because Linux limits thread names to 15 bytes (not including the required terminal nul).
Other unices are better though not great, macOS is 63 I think.
By comparison Windows allows 32… kilobytes.
Though “runtime” is probably what should be shortened here, “rt” is literally the name of the tokio features for configuring the runtime, so that would likely be more understandable than the trailing “-w”.
Amazon link: https://www.amazon.com/dp/1492052590
Direct link to the chapter (if you subscribe to O'Reilly/Safari): https://learning.oreilly.com/library/view/programming-rust-2...
This seems to be more of a hands on guide for the ecosystem.
You create a very minimal Redis clone progressively by swapping out synchronous for async.
I like fasterthanlime's writeups but it does have one particular bugbear for me, that seems particularly common for Rust (and Python, to be fair) feature writeups: environment-based yak shaving because the author likes the mostly-unrelated library <X>. The very first thing that happens if I try to reproduce this code is a build error for cargo-edit, which is totally unrelated to the feature being taught, yet if I wanted to follow the writeup, I'd have to fix it.
I do really have this bugbear, particularly with Rust tutorials
I can definitely see some ML/Haskell/Scala/etc. folks who would quickly pick up on all the Future stuff, drawing parallels with their usual language of choice, yet being completely unaware of all the nice tooling around Rust. It's one of its main selling points for me!
Re cargo-edit specifically, it also lets me stay in flow while writing these: instead of having to repeat "we'll just edit Cargo.toml, make sure to have that under [dependencies], uh this time we need a feature so the right-hand-side needs to be an object, not a string" - instead I can just copy into the article the actual commands I run on my side, and if a quick paragraph about where that subcommand comes from unblocks even one person then it's worth it.
So yes, it's a long article but I didn't mind that in the least. Keep it up!
I really enjoyed reading this piece, and had never heard of cargo-edit, so thank you for that.
As a Scala person, I've actually struggled with Rust's Future, because it's quite different and requires a new mental model of how things work, which I think you did a great job providing.
My colleagues. Many people are skilled programmers jumping into obscure problems with only a baseline level of familiarity with the greater environment.
cargo-edit looks useful. None of the tutorials I've skimmed have mentioned it.
raises hand
I'm not sure how (or why) I'd google cargo-edit, because it's one of those things that you don't even think about until someone tells you about it, and then you're happy they did.
While I certainly wouldn't call myself a Rust expert, I'm no noob either, and am intensely interested in diving deep into topics like this. (Can we also dispense with the gatekeeping put-downs like "noob", please? It's unnecessary and rude.)
> It’s ok to teach both things but why together?!
Because the author felt like it, and since we're consuming his work for free, we're not entitled to tell him how to write?
I'm sorry yodelshady has run into a build error for cargo-edit (was it OpenSSL? screw OpenSSL) but also: the more folks run into these issues and report them, the sooner they're fixed. Also! I think cargo-edit should just be adopted by the default cargo distribution because it's all-around really good.
[1]: https://www.lpalmieri.com/posts/2020-09-27-zero-to-productio...
Many, many tutorials make this mistake.
In their defense, the title is "Understanding Rust futures by going way too deep", and I think advice and tangents on best practices modules is easily covered by "going way too deep".
> you're doing your readers/learners a disservice.
Not every tutorial needs to be the same, cater to the same type of person, or try to explain things in the same manner. I think the bigger disservice to readers/learners would be to homogenize tutorials into what you think is best. That might work best for a certain type of person at a certain skill level, but that doesn't mean everyone will be served well by that. And as the comments here illustrate, it's not like there's a dearth of Rust futures tutorials and explanations. There's definitely room for a rambling, informal, irreverent, meandering take (it's the only thing that kept me reading until the end, or even past the first page or two).
I enjoyed it, and I had never heard of cargo-edit before, and am really glad I now know about it!
I too really, really want this. If only I had the energy to work up some patches...
Yes, and if I had learned about these a couple months ago it would have saved me ~8 hours tracking a bug down. So, thank you.
Futures was stabilized before the async syntax, so this used to be the only way you could do it. But those were definitely the bad old days.
A handful of (brave) developers and library writers wrote async code like this back then when they really needed to, but the vast majority waited for async syntax to stabilize. Those state machines get ugly real fast.
While that's technically true (std::future was in 1.36 and async/await syntax was in 1.39), that's a very short timespan of only a few months. You are probably referring to the multiple years where futures only existed as addon libraries where their design was iterated on before the final design of std::future was settled on.
If you need to instruct users to install a compiler or an extra tool, so be it. It just must work as-is on a clean system (for which using Docker is easiest for me, but a clean VM or any other alternative is fine too).
This is how my docs for Zapatos[1] work, for instance. :)
Very much this. We often write scientific software with (command-line) instructions for nontechnical or sort-of-technical users and have learned from experience to not assume anything about the user's environment, or background knowledge, and to try to replicate our documentation's instructions line-by-line!
Do you think it's a good fit? Use-case-dependent? Maybe for things like RF?
I'm very excited about this for one simple stupid reason: sleep(). Awaiting a timer delay deep inside some code is gonna be amazing. With typical sync code, you basically have to split your code and queue up the next work somewhere that would be picked up by the timer interrupt… basically doing the whole state-keeping that async would do for you.
Except that DNS apparently still blocks[0], so usually things farm DNS requests off to their own blocking thread pools (the author ends up disabling DNS just so they can "prove" everything works with just a single thread)
What's so fundamentally difficult about writing an async/non-blocking DNS resolver? Is it just a lack of a real need for it?
[0] (quote: People have been trying to build asynchronous DNS resolvers for decades without success. Can it be done? Yes. Has it been done? No. ) https://gist.github.com/djspiewak/46b543800958cf61af6efa8e07...
DNS seems like it perfectly fits the model of "give me a buffer (to hold the resolved IP), and I will wake you up when it's filled". Maybe io_uring (/ IOCP?) already can support this, and these articles are just mistaken about the current (or soon-to-be) state-of-the-art? Or is there some fundamental reason about DNS that make writing a non-blocking resolver very very difficult?
It's just very odd to me that userspace apps are creating their own blocking thread pools just to run DNS stuff, when they can do seemingly everything else with just a single thread if they wanted to.
(edited a few times, sorry if I caught someone who was mid-reply)
Not really.
"Blocking" means "tell the scheduler to mark the process idle until operation finishes", while "non-blocking" means "don't mark the process but flip a semaphore bit when operation finishes".
The point of view of the OS the difference is just which bit to flip.
It has weird consequences, e.g. you can't use a named pipe in 2 threads doing read, write separately(without doing protcol level coordination say in a proxy). You need to use asynchornous i/o to get it right.
I think I have a lot more to learn about DNS...
[0] https://github.com/axboe/liburing/issues/26#issuecomment-738...
> [re: implementing getaddrinfo] It's not planned because it's not a single operation but a complex beast reading files, exchanging messages, etc. IMHO, as it's not a single operation it fundamentally doesn't fit liburing, but implementing under some framework on-top would be more viable.
NodeJS has a built-in DNS resolution API which is fully async / libuv-cooperative: https://nodejs.org/api/dns.html#dns_dns_resolve_hostname_rrt...
Other async runtimes bundle libs like c-ares to implement non-threadpool-based async DNS.
quoting the nodejs docs: "These functions are implemented quite differently than dns.lookup(). They do not use getaddrinfo(3) and they always perform a DNS query on the network. This network communication is always done asynchronously, and does not use libuv's threadpool."
Having 1000 dependencies sounds crazy to me! If there's a bug in even one of them that affects you then there's gonna be a lot of digging to figure out the cause, and possibly multiple pull requests to get it fixed. I think my CPU would spend a bit longer than 10-15 secs on the linker step, too.
* Incremental compilation helps locally, not so much in CI, unless you save/restore the whole cache which gets large quickly and evicting the out-of-date objects is non-trivial.
* In CI, sccache helps, but not as much as I'd like: there's a bunch of things that are non-cacheable, notably crates that pull in & compile C/C++ code (I'd trade 2 c-bindings crates against 12 Rust-only crates any day for that reason alone)
* Splitting stuff across different crates helps parallelizing the build (and caching it better), which may be one of the reasons some projects end up having "1000 dependencies" (although I've rarely seen upwards of 600)
* For incremental builds, linking does become the bottleneck. Full LTO is especially slow, Thin LTO is better. Switching to LLD improves thing. I'm hopeful that mold will improve things some more.
* Re multiple PRs: thankfully crate families tend to live in the same repository, so fixing something across both tracing / tracing-subscriber could be a single PR, for example.
I wish the Rust community invested more in build caching, but even with the current state of things, there's often steps you can take to make things better.> I still feel uneasy depending on so many other crates, but this seems to be a level of paranoia that others in the community don't share. Having 1000 dependencies sounds crazy to me! If there's a bug in even one of them that affects you then there's gonna be a lot of digging to figure out the cause
is that there is far, far more likely to be a bug in the version you write on your own to achieve the same goal than there is in a widely-observed library written by somebody who's chosen to specialize in that specific thing
Just look at how many C and C++ libraries are maintained by 1 individual and have almost no 3rd party oversight to see that Rust can't automatically make the claim you made.
That all said, for anything complicated and/or directly security related, one should always check if there is a module first.
I do acknowledge that there will always be bugs that are identified by your users but equally if you're not auditing your dependencies first then it's hard to argue that you're not just passing off that responsibility wholesale to your users.
In general I agree with this, but there is another relevant aspect to consider: something that I've written on my own for a particular project is also likely to be more purpose-built, and therefore simpler.
My time is worth more than my computer's time.
I was thinking that too, but then I did a quick check of a few of my Java services for work, and many of them have more than 400 dependencies (most transitive, of course), with a few getting up to 600.
Obviously comparing Java and Rust is not exactly apples to apples, but I rarely care much about the number of dependencies in my Java projects, so I guess I shouldn't be too worried about Rust projects either (aside from link time, I guess, which javac doesn't have to do).
It's all cached, so consider it more of a library install time.
pub fn try_join<A, B, AR, BR, E>(a: A, b: B) -> impl Future<Output = Result<(AR, BR), E>>
where
A: Future<Output = Result<AR, E>>,
B: Future<Output = Result<BR, E>>
I get why its like that, but I really wished this wasn't a typical Rust generics signature.I see why Haskell folks like their type level functions.
Nowadays the editor would help you do that. When I'm coding Rust the editor shows the types inline in a small font, which is very helpful.
Conceivably if there was a way to not repeat the future parts.
I meant "you will only encounter this level of generics complexity in Rust" (disclaimer: I don't know C++). They can't be that rare either, I have not written a lot of Rust but already encountered similar constructs multiple times. But admittedly that might be partly due to no-std requirements.
[1[: https://docs.microsoft.com/en-us/dotnet/api/system.tuple-8?v...
pub fn try_join<A: Future<Output = Result<AR, E>>, B: Future<Output = Result<BR, E>>, AR, BR, E>(a: A, b: B) -> impl Future<Output = Result<(AR, BR), E>> {pub fn - public function
<...> - declaration of types,
A - type of first future,
B - type of second future,
AR - type of result of first future
BR - type of result of second future
E - type of error
So, public function try_join accepts two futures, which my return <AR>esult and <BR>esult or <E>rror, and returns future which will return tupple with (<AR>, <BR>) or <E>rror.
pub fn try_join<A, B, AR, BR, E>(a: A, b: B) -> impl Future<Output = Result<(AR, BR), E>>
where
A: Future<Output = Result<AR, E>>,
B: Future<Output = Result<BR, E>>,
Come on... "A", "B", "E", "a", "b"??? Naming stuff like this is the standard way? I mean, if I know nothing about what the code does, and I find this kind of function, I'll start swearing right away.Also, the function only does:
TryJoin::Polling {
a: State::Future(a),
b: State::Future(b),
}
Which is clearer and shorter. Why even writing a function signature like that? It's longer and more complicated to understand than the code itself! I don't know much Rust, but since a semicolon is missing this is just a return value, right?So why not?
let res = TryJoin::Polling {
a: State::Future(fetch_thing("first")),
b: State::Future(fetch_thing("second")),
}.await?
PS: I don't want to criticize, and I'm not a Rust literate, I'm just amazed with its complexity.- The example you gave at the bottom won't really work, because enum variants aren't fully-fledged types on their own and they can't impl `Future`. Therefore, you can't `.await` them on their own. Furthermore, the point of `try_join` is to poll both futures until one fails, which you can't do with `async` syntax.
- Rust requires that you spell out all generic parameters like this at function boundaries. It's technically possible to infer types across such boundaries, but that has the potential for significant changes to the typing of a function to go entirely unnoticed. In fact, I believe some of Rust's documentation specifically calls out problems Haskell had with inferring function signatures like this.
That being said, you are correct that we could do this instead:
```pub fn try_join<FutureA, FutureB, ValueA, ValueB, Error>(future_a: FutureA, future_b: FutureB> -> impl Future<Output = Result<(ValueA, ValueB), Error> where FutureA: Future<Output = Result<ValueA, Error>>, FutureB: Future<Output = Result<ValueB, Error>>,```
That is slightly more readable.
At this point, somehow, I'm starting to consider C++ templates beautiful! Perhaps a signature form like:
template <FutureA, FutureB, ValueA, ValueB, Error>
pub fn try_join() ... etc.
would be clearer at this point.No, it's verbose and ugly. Easy to read would be something like:
fn try_join(a:A:Future<E||AR>,b:B:Future<E||BR>) -> impl Future<E||(AR,BR)>
(Rounding to the nearest Rust-like syntax where appropriate and borrowing `type (||) = Either` from Haskell.)How would you have preferred it looked?
Edit: Whoever is downvoting, Iosevka is one of the favorites of the HN crowd, and also my own daily driver. It is worth taking for a spin if you haven't yet.
It's great to have pure futures-returning functions and being able to isolate side effects easily.
I find it much easier to reason with (coming from mainly JS, where everything is executed as soon as a promise is created).
Iterators change over space, Futures change over time.
In iterator, every time you call 'next' you get a value until the end when you get nothing. In Future, every time you call 'poll' you get nothing until the end when you get a value.
For me Rust is "simple" because it has great tooling / great diagnostics, so when I get something wrong, most of the time it can tell me exactly what it is (and how to fix it!), and only occasionally does it send me down a rabbit hole.
Rust is also "simple" because it's memory-safe - if I can get something to compile (without unsafe code), then I know there's no use-after-free, double-free, buffer overflows, etc. I can concentrate my mind on the business logic (where bugs still can and do happen).
Finally, Rust is "simple" because it lets me build abstractions so I can see more clearly what I'm doing. At work we had a Go codebase full of goroutines and channels - I got tired of it and did a proof of concept in Rust: now it's just one async Stream, you can see the data flow clearly and worry about each stage of the pipeline in isolation, instead of having to jump across many different source files.
But no, it's not all very simple. There's plenty to learn, and some of the core concepts (ownership / lifetimes) are fairly hard to wrap your mind around, even if you've encountered something similar in other languages. My take is that it's worth it - you'll be very slow at first, but as you get more comfortable with it, so will you get faster.
In my opinion it really depends on your background.
For example, Rust uses the `i32` type as the standard integer. If you come from a C/C++ background this is probably not that weird, you probably know how many bits there are in 4 bytes off hand, there's `int32_t` standard types and you probably have experienced not wanting an integer with a variable size that depends on the platform.
If however you come from Python, Javascript, or maybe even Java, this might range from a little weird to very weird. Python only has the `int` type, no unsigned types and you might not know how many bits make up a "standard" integer, or even exactly how bits, signedness and integer sizes fit together. Java doesn't have unsigned types and doesn't have issues with sizes depending on the exact platform, so they might not understand why you would want the amount of bits right there in the type name.
This is just for the standard integer type. If you consider how many design decisions are a direct result of working on low level, high correctness required C++ code, the "simplicity" could range from completely simple to very much not simple.
I guess you were trying to disparage Yehuda, then?
An evangelist/growth focused role is exactly what a promoter is. If you spend large numbers of typical work-hours, which I'm assuming you're paid for, on social media to promote something, like Rust, or you go to conferences and events to speak publicly in promotion of something like Rust, then a "paid promoter" seems like a pretty accurate description.
Is that disparaging? Is that not what you are doing right now?
Right. move into. Because that was not a part of my job description at Mozilla. They didn't dislike the stuff I was doing, but it's not my work.
The disparagement is the implication of lying:
> You can judge for yourself if their promotion of rails was grounded in truth.
In this case it implies dishonest astroturfing or similar, especially when you talk about whether their statements were "grounded in truth". (Which is different from whether Ruby did well years later.)
Rust is "simple" in that it is based on a few solid principles. Data with layout, Ownership of the data and the distinction between sharing and mutability (through immutable references and mutable references). Most of the interesting API in _some_ way leans into that. So, the _basics_ of Rust are indeed quite constrained.
But that's "simple" in the sense of "Ruby is simple, everything is objects and message passing".
But then: Generics introduce a ton complexity on top, because they open a space where you are _potentially_ in a borrowed _or_ an owned situation. There's tons of tiny optimisations and protocols: for example that `Box<[u8]>` exists and turning it into `Vec<u8>` comes basically for free. There's tiny details like compiler assists that sometimes trigger and sometimes not. But even Generics can also be seen as an application/extension of the basic principles.
And then, there's tons of tooling and language that you need to know. Standard traits and features of the stdlib. Clever applications of Ownership based management for handles.
And particularly Rust is (currently) not _easy_, because the programming environment has fundamentals that are unusual to program that you cannot bypass as a beginner. That actually changes, the more those practices get known to people and individuals can teach each other rather than finding out themselves.
I think what people say when they say "Rust is simple" is that they can break down any house to a limited set of bricks. That is certainly true. But that needs a level of proficiency with the language. So, telling beginners "look at Rust, it's simple" is harmful - it's full of combinations and applications of its language core and it will take quite some time to learn breaking that down for people and figure out which tool is for which moment.
>And that's a fact. You can quote me on that because, well, who's going to go and verify that? That sounds like a lot of work. Just trust me on this.
Sure, but I click away when it hurts to read.
Rust doesn't merely lack a language standard, its reference and advanced documentation is also a very unfunny joke. You can learn just enough to have some idea of what you're doing, and then you're stuck huffing unicorn queefs because nobody including the compiler devs knows the exact rules on how syntax is supposed to work.
Magical shortcuts, syntax, and borrow checker tricks are added and removed at random with no rhyme or reason, and Rust severely punishes the programmer for wanting to know how the language actually works, since there are usually no answers, or worse, wrong answers, for them to find.
I still use Rust because the memory safety without a GC is wonderful, but please, don't even try to make it look like Rust has some official documentation or behavior. It most definitely does not, and what really disturbs me is how so many of the Rust community don't even want a standard.
So OP, everything you've just done, will be rendered obsolete and woefully wrong in a couple years when Rust serves up another opaque unicorn-burger.
I should mention that even when they adjust these things in a stable release, they never fix the documentation to reflect it. And in the case of many of these things, they aren't documented at all, except perhaps a couple sentences about their existence.
This can only be fixed by A. Requiring that all syntax changes, no matter how small, are fully documented before they reach stable or even beta, and B. Eventual standardization, even if the reference implementation ends up being more cutting edge.
Rust devs also need to think more about how they're going to document a feature before they add it. Like for many of the implicit reborrows, they're done in non-obvious places and not done in others, with no pattern to where it's done and where it's not. That kind of implementation is extremely painful to document or standardize. Rust needs to think more in terms of rules for syntax, rather than instances of a particular shortcut. Rather than adding a standalone instance, you should edit the rules so that instance is covered.
I also want standardization to make writing alternative conforming compilers easier. I strongly dislike the attitude of some Rust maintainers, and a standard is a useful tool to forcibly remove some level of control from their hands.
The top talks about lt, le, gt, ge, but then the docs below talk about '<' and '>', which I assume are lt and gt? But then there seems to be no requirements in le and ge? I assume everything needs to be consistent?
Also (relating to auto-ref), why does 'a<b' work, when the trait takes a reference?
EDIT: Some of this is revealed if I scroll down and look at the definitions of 'lt' and friends, but I'm already pretty confused at the top.
EDIT 2: This really wasn't worth the effort of defending, but I made an issue with my comments on PartialOrd.
- what do you think a language standard is or does
- what languages with standards do you think standardization has helped (this is a trick question! do not answer "c" or "c++"!)
- in which languages with standards is a plurality of code written to that standard? (this is also a trick question! do not answer "c", "c++", or "javascript"!)
The rust docs don't currently meet that bar. They're quite WIP, and I imagine someone hitting a particular edge case will not necessarily be well equipped to understand whether the behavior they're observing is intended, a bug in the implementation or something else entirely.
There is a great deal of written information about Rust-the-language which exists only in compiler comments, RFCs, bug reports and internals.rust-lang.org threads (often slightly stale in all cases).
It's Rust's greatest weakness.
But "every time this comes up" the discussion somehow gets sidetracked into arguments about standardisation, even in cases (such as this thread) when it's clear that what the ranter cares about is accurate documentation.
Even though C++ code in the wild is filled with standard violations, I like having a standard explaining what rules exist, so developers can reason about them, and I can use them to tell other developers to change their code because their code is invalid. (In practice I don't know if violating strict aliasing through reinterpret_cast rather than bit_cast can lead to compilation errors, and sadly std::variant can miscompile due to broken aliasing optimizations in library code even with valid user code: https://www.reddit.com/r/cpp/comments/j7gn2d/stdvariant_is_b....) Currently, not all unsafe Rust can be classified as either valid (make sure the compiler doesn't break it) or invalid (change the code until it's valid), though there are some cases where unsafe Rust is clearly invalid.
Maybe it's a temporary state while the language team makes decisions based on how the unsafe library ecosystem progresses. Maybe the language team has no plans to put the language on a firm groundwork defining exactly what is legal or not, which I find uncomfortable (though maybe others feel more comfortable with it). In any case, there's not that much practical impact on the quality of code being written and binaries being produced, but it still hurts to see people building castles on uncertain foundations.
Personally I disagree. If you want to write freely aliasing code, I think it's better to use &UnsafeCell or &Cell or &RefCell, perhaps with an Arc (or my AliasPtr crate) instead of trying to convince rustc that &mut isn't exclusive.
Everything else aside for now, this is just patently false. There's a well-established RFC process for any such changes to the language/compiler/standard library.
Development languages evolve all the time. It's normal, the same happens to human languages.
I can't even say Rust is changing that much (especially compared to JS, my main work language).
I have taken a look at the Rust documentation, and I just can't see myself participating at this point because of it. The roadmap is also unclear to me. I google 'Rust Roadmap', and guess what the first result is?
So far a video game's roadmap is ranked above that of this language. I had to search for 'rust-lang roadmap' to get a good top hit... Which brought me here:
https://blog.rust-lang.org/2020/09/03/Planning-2021-Roadmap....
> The core team is beginning to think about the 2021 Roadmap, and we want to hear from the community. We’re going to be running two parallel efforts over the next several weeks: the 2020 Rust Survey, to be announced next week, and a call for blog posts.
A call for blog posts. Beginning to think about...
As a .NET developer, this honestly sounds like a dumpster fire to me. Who is actually in charge? What is the vision for 2025? How do you explain your software technology roadmap to your customers when you are using Rust and have to operate in contract lifetimes of 5+ years?
Clearly, I don't know the first goddamn thing about the Rust ecosystem or why I would want to use it. I am just a dirty .NET plebian looking in from the outside. It doesn't look very compelling right now. Can someone correct my perspective on this? I am open to it. There must be some value I am missing. Maybe Rust just isn't for the "enterprise", or whatever boring niche box I seem to be stuck living in these days.
There are obviously use cases and happy developers out there. Don't let me get in your way.
https://www.rust-lang.org/ has a big old "governance" header on it, which links to this page https://www.rust-lang.org/governance
That page explains the process, who is in charge of it, and links to the roadmap for this year.
2021 is a pretty chill year for Rust overall. The project is largely focused inward, the Foundation is newly launched and figuring itself out, the edition is going to be much smaller than in 2018, and most things that are shipping are polish, bugfixes, and small API improvements.
> What is the vision for 2025?
We only plan in year increments; it is extremely difficult to do longer-term forecasting than that in an open source project. To be honest, even yearly is hard to do with more detail than high-level goals.
Why not wait and figure that out when we get there? The world is changing too fast now; consider that any vision for 2020 written in 2015 would probably have been useless by the time we were actually in 2020.
Edit: Also consider that updates to Rust probably don't have nearly as big an effect on your customers as changes to .NET, especially the old .NET Framework which was shipped as a shared runtime environment on the customers' machines. Assuming you ship binaries to your customers, Rust probably isn't even visible to them.