Having the new borrow checker finally on in all cases is nifty. For valid programs the behavior is the same as the previous version, but in the event of a compiler error, previous versions would still show you the output of the old borrow checker. I've already had one case where someone came asking about a confusing borrow checker error message and I just had to suggest that they try compiling with 1.63 and the new error message was clear enough that it answered their question immediately.
Now we just need to do it all over again once Polonius finally arrives. :P
Migrations aren't ideal, but I'm excited to see continued innovation in borrow-checking. This is a major area where Rust is exploring truly new territory as a language, so growing pains are to be expected, and it's encouraging to me that Rust's semantics allow borrow checking to keep getting better without breaking changes
> it's encouraging to me that Rust's semantics allow borrow checking to keep getting better without breaking changes
Just to be clear, these are breaking changes. If the new borrow checker didn’t break existing code, they would have removed the old one long ago. They’ve just decided that the observable effects of the breaking change in the ecosystem are small enough (especially after warning about them for years) that they are okay making the breaking change without a semver-incrementing Rust 2.0.
To be fair, the reason for the breaking changes is generally to fix soundness issues (of course, not every example of code now banned will exhibit unsoundness), but that doesn’t cover all of them. There were some completely sound (albeit useless) examples of partial initialization that broke with NLL:
Sure, I guess all I meant was that the fundamental semantics of borrowing and lifetimes that were chosen at the beginning have continued to scale up to a better and better borrow-checker experience. They haven't found deep limitations of the language that prevented innovation without going back to the drawing board.
I'm probably mixing different things up, but I thought the non-lexical lifetime changes made the borrow checker more permissive in the sense that some code that the compiler would reject using the lexical borrow checker, would be allowed using the non-lexical borrow checker.
If this is so, how are they maintaining edition compatibility? It seems like you could write code for the 2015 edition that would build with the new compiler, but not an older (2015 edition compliant) compiler.
The new borrow checker both allows more to compile (by being more precise about tracking control flow, allowing it to prove more advanced things about lifetime usage) and also less code to compile (again, by being more precise about tracking control flow, allowing bugs in the old borrow checker to be fixed (see https://github.com/rust-lang/rust/issues?q=is%3Aissue+label%... for examples)).
The edition compatibility story here is interesting. When the 2018 edition came out, the new borrow checker was only turned on for code on the 2018 edition. Meanwhile, in the 2015 edition, it would run both borrow checkers and yield a warning if your code passed the old borrow checker but not the new one. After a migration period, the new borrow checker was eventually made the default on the 2015 edition as well. Because of the aforementioned soundness bugs fixed by the new borrow checker, this was seen as acceptable (soundness fixes are explicitly allowed by the stability policy, although the Rust developers still try to provide migration periods to make them less painful).
Editions only guarantee that old code builds with new compilers, not vice versa. New features are added to all editions unless they require e.g. new keywords that are available only in new editions.
Ah, that is right. I apologize, I think I've learned that three times now, but for some reason my brain refuses to accept it.
I like that previous editions get new features when possible, unlike say C++, but I wish they'd gone with semantic versioning for the language version, like python did when both 2 and 3 were active. Oh well, I just need to remember that the edition is like the major version, and compiler version is sort of like minor version, and both are needed to specify forwards compatibility.
It might make more sense to think of editions as optional changes to the surface syntax, and nothing more. They are not nearly as important as the name makes them sound.
They are often small, but it's a mistake to say they're surface-level. Changes to syntax or semantics only get made when absolutely necessary to allow for essential features or fixes.
Editions are orthogonal to compiler versions. It is perfectly fine if code builds with a new compiler but not an old one. Another example of this: you can write code in a 2015 edition crate that calls the new `std::thread::scope` function.
Scoped threads are something that really showcases Rust's features around thread-safety, lifetimes, sharing and mutability. I'm glad it's finally back in the standard library!
I would be very interested in hearing from experienced folks about how important this is/what useful patterns it unlocks.
I ask because I remember when trying to learn Kotlin, the docs made a big deal of structured concurrency[0], and golang of course is known for its concurrency story.
Sadly, this is only lexical scoping which means you can only borrow from the outer scope and not ad-hoc the normal way borrowing works in Rust.
In short, this means that thread pools and async runtimes cannot use this feature to provide borrowing across tasks. We're still stuck with Arc-ing everything that crosses task boundaries.
Prior to 1.0, there were "true" scoped threads (with a lifetime 'a on the join handle), which let you borrow from the parent thread. (This could have extended to tasks as well, had they been around.) unfortunately it wasn't safe together with the much more popular Arc/Rc, which wasn't 'static restricted, despite it's ability to cause cycles and leak. So those powerful scoped threads were removed and "destructors are not guaranteed to run in safe Rust" was formalized instead.
Seeing how popular async became, I wonder if this was the only and right way to move forward. I'm not against Arcs, they have their place. But if you can use normal RAII it's much preferable.
How is this different from the prior 1.0 scoped threads? I remember the whole "Leakpocalypse" event that lead to the deletion of the API, (and subsequently, to invention of this API, where the scope guard is represented as an additional closure) but those were entirely lexical too, right? Rust borrows in general are pretty much lexical in the sense that you only analyze the body local function, nothing more. Rustc can't reason about dynamic invariants, you need runtime checks for that.
Probably some sort of flexibility / infectivity e.g. let’s say you have a function a which calls a function b with some borrowed data, b wants to spawn and return a thread (possibly wrapped in some other structure).
In the original acception I think you could literally do that, a scoped thread would just be a normal RAII object with various lifetime bounds.
With the new scoped threads, I’d think you need to create the scope in a and pass the scope handle to b so it can spawn the thread correctly.
> async runtimes cannot use this feature to provide borrowing across tasks
They can't use this function, but with some duplicated unsafe code they can still provide similar APIs for their callers, right? Things like this: https://github.com/jaboatman/tokio-scoped
I just checked quickly so may be wrong, but it looks like they're pulling the same trick as with the new scoped threads, ie only lexical scoping. Note also that the scope function is blocking and called from within an async fn, which is normally not great for composability with other async code.
That said, taking a step back it has crossed my mind that bringing back true scoping with unsafe in the impl might be an acceptable way forward, despite how upsetting this is to a lot of rustaceans. I think that the failure modes (ie triggering UB) for this case can be simply enumerated in a short list of "DON'Ts" (such as putting a scoped join handle in an Arc).
OTOH, since leaking was formalized and explicitly allowed (heck, even borderline encouraged through mem::forget), god knows if people have exploited that for other cases in true Hyrum's law fashion, that breaks the abovementioned idea. They certainly had the right to.
> I would be very interested in hearing from experienced folks about how important this is/what useful patterns it unlocks.
For Rust it allows thread to play better with lifetimes, which means there are scenarios where you don’t need the overhead of a lock (and usually the Arc that does with it, unless you go with a global) or queue, you can just work with on-stack “unsynchronised” borrows and it’s thread safe.
That means lower syntactic and runtime overhead in the cases where it’s an option.
It was already available in crossbeam so it’s not novel novel, but not needing a dependency for it is useful still.
Rust is the greatest. It’s my go-to for virtually anything that it could reasonably be used for (a lot). After using it exclusively for a while and getting comfortable and fast with it, I can’t see any good reason to use anything else for most purposes.
Yeah GUI is really the biggest missing part of the ecosystem. To be fair it is an extremely difficult problem; there is a reason there are so many electron apps out there.
A lot of times a cross platform non-native GUI is the right tool. Same UX on every platform. What's not to like? Allows faster iteration for small teams, you can use TS and doesn't even have to think about Swift/ObjC/Java/Kotlin/C++, and you can use whatever you like for the backend.
Speaking as a user, that's exactly what I don't like. I don't care about same UX on every platform; I'd rather have consistent UX across apps on the same platform.
Sometimes you work on a platform that does not have a native style. (Eg. kiosks.) Or the native app is just an extension of a very website-driven service. (Eg. an app for event management, ticket admissions.)
As a user I prefer something that works, making sure one thing works consistently is easier than testing different native things that implement the same functionality. (Oh, and as a user I very much prefer that it does implement the same functionality.)
Native interfaces and behaviour are preferable over whatever options the app developer chooses to support.
In the case of electron apps, effectively running multiple browsers is also in the “what not to like” basket.
Native apps often have less input latency, and are significantly nicer to use.
> Allows faster iteration for small teams
As a user, I’d much, much prefer slower iteration if it meant the resulting app was better. Electron apps thrashing out pointless updates every other day just because they can isn’t always a positive in my book.
What's the native interface when Apple/Google/Samsung changes that every year (or two years)? What's the native interface of a kiosk at a car wash? (Where we used a touch screen running full screen Firefox and the users saw the same thing as they saw on their phone where they managed their wash coupons and whatevers.)
On desktop I don't run slack, discord, teams (and whatever wants to run separately) separately, if something that doesn't do meaningful local I/O and/or computation cannot run in a browser, then I don't use it if I can avoid it at all. And I hopefully can keep my streak of not developing nor shipping any forced electron based mess.
VSCode is fast. I'm surprised too. While native Windows things are just slow all over (with a beefy modern desktop).
Seriously, how the fuck can the start menu be this ridiculously slow? I have like 50 things installed, and I use about 5 of those. And it takes forever to find those as I start typing. (I have KDE flashbacks!)
Oh and super native Firefox is also a disaster when it comes to speed (I search for the same bookmarked pages in the awesomebar - the combined location and search bar, I have disabled the search part, because the UX was horrible, so it should just search in the local recency shorted LRU cached list of pages ... and it's dog slow and dumb).
Big billion dollar unicorns push those meaningless updates, not small teams (at least in my experience).
Yes, I want to do that, but Apple, Microsoft, and Google (as Android) continues to veto that. And there are some arguments for why (the advantages of the walled gardens of the "curated" platform stores, the tech inertia of the different APIs of apps vs websites, and the learned user behavior).
Mozilla tried "Firefox OS" and it's hard to do it. Chrome tried service workers and persistent sites/apps, but.. it's just lame compared to shipping a separate app that bundles a web view and a backend.
Probably a lost cause at this point since most GUI toolsets are too object-oriented to comfortably wrap and call with Rust. Do a web view with Tauri instead.
I haven't found any problems using GTK bindings in Rust. It is actually easier and nicer than in C, especially with additional support like relm4 (but even the manual approach is good enough). Who said GUI must be OOP?
GUIs don't exist by themselves, but serve a program. Rust may still be the best language for the non-GUI part of the program.
I've once picked ObjC for a whole program based on its native GUI, and spent way too much time chasing bugs caused by its thread-unsafety (Cocoa bindings are a trap in multi-threaded programs).
I have some serious FOMO with Rust... I have been using JS and Python for everything the last few years. Will you please recommend some Primers/Resources/Tutorials for Rust for someone like me? That is, basically a script basher that glues everything together with Python.
JS and Python are my primary languages. I've toyed around with Rust, and it is pretty cool. There were a number of nice moments where I was surprised with how much it felt like I was writing high-level code. However, the overall learning curve is huge, especially if one is mostly used to garbage-collection (like me).
If you are doing web stuff, I'm sure Rust can be used effectively. But it is non-trivial and there are serious tradeoffs (learning curve, lack of ecosystem for web, hiring, etc). Teams would be better off sticking with python or JS for most web projects. Some perf-heavy backend or infra projects could possibly benefit from Rust, but otherwise it doesn't seem like the best choice to me.
It is definitely a cool language that seems to be gaining serious momentum. Could be fun to mess around with.
I just read the book (the Rust programming language, free online) cover to cover and started porting all of our Python (command line) apps. It was much, much easier than the internet led me to believe going into it. Just take the time to really internalize them ownership stuff… the rest is syntax and patterns.
Rust is probably great for systems programming, and might have other nice, modern features that make it compelling outside of that niche, but as a web developer, I simply don't care about manual memory management and fighting with the borrow checker.
I understand why it exists for certain tasks, but I can just use a garbage collected language instead (preferably a modern and expressive one, like Kotlin) and save myself a lot of trouble, because in the end, the performance hit often doesn't matter.
1. The versatility of Rust means I can use it for truly "the full stack". E.g., in my current side project, I'm using rust to do audio decoding, resampling, running a speech recognition model, the backend of the app, and the frontend! Though I haven't yet started on all of those components, I expect to get some major simplification in my code because of this.
2. The "modern and expressive" languages all have fatal flaws, _for me_. Swift has a poor cross platform story/community, Kotlins tooling is poor if you don't use intelliJ, ocaml has a reputation of poor documentation. I understand that not all of these things will bother people as much as they bother me, but they do bother me a lot. Tbh, I'm still on the lookout for a lang that is actually statically typed(no TS or python), has an expressive type system, a good cross platform story, good, non proprietary tooling, and is at a slightly higher abstraction level than Rust.
1. sounds like a project where Rust is indeed a good fit.
As for 2, I'm very happy with Kotlin for the most part. I agree that there's no point in trying to use it without IntelliJ, but... IntelliJ is really good, and the community edition is free. Yes, the startup times can be slow and annoying, but what I get in return is IMHO worth it (even more so if you use Ultimate and frameworks like Spring). Also, command line tools for Kotlin are IMHO better than for Java, at least (e.g. ktlint). I agree that Swift outside of Apple software is a dead end.
Given your constraints, though, why are you ruling out TS (I've never used it, but I heard good things)? And what about Haskell?
Though, in all fairness, in your case you're probably comfortable enough with Rust's borrow checker that it doesn't slow you down that much anymore, so I understand why you'd continue using it. For someone who has never worked with it, it's a different story.
My (admittedly poor) excuse is that I really wanna keep using neovim (I really like the customizability) :D
> Why not TS/Haskell?
TS is amazingly expressive, and probably my choice after Rust for traditional full stack! But it's sometimes the case that the underlying dynamic nature of JS "leaks through" (especially when using third party libraries), and it does come with the rest of the JS baggage (undefined == null, no easy pattern matching, ..etc).
Haskell? I just realized that, from reading the interwebs, I've had an unconscious bias against Haskell as a mostly academic language unfit for industry use cases except in very narrow niches, but I def should check it out and make up my own mind.
> Though, in all fairness, in your case you're probably comfortable enough with Rust's borrow checker that it doesn't slow you down that much anymore.
True. On the rare occasion that it does, I just use the "cop out" of doing `.clone()`. I hope to get better though!
> My (admittedly poor) excuse is that I really wanna keep using neovim (I really like the customizability) :D
I used to be a big "vim for everything" person. Now, less so. I just use vim mode in my editor/IDE of choice, because what I'm really after is the superior text editing. Now, depending on the project and language, I'll use vim, vs code or IntelliJ. But I can see that if you're really into all the vim customisability, it's different...
Building a basic rest api with rocket is no more difficult than doing the same with Sinatra or flask once you have the hang of it. Arguably much easier if you count time chasing down bugs from unexpected runtime behavior.
What is everyone building in rust, and how do the killer rust features (mentioned in other comments) help you?
I'm not anti rust in any way, it's more of having limited time and so many languages problem. But I'm interested in the types of problems rust is good for, so I could consider it if/when I encounter them
I've built a fair amount of production software in Rust. The best features are:
- Rust is naturally pretty fast, on the rough order of C++ in many cases. Even unoptimized programs tend to be very snappy (as long as you remember to compile in release mode and buffer I/O).
- Rust tends to warn me if I make a dumb mistake, rather than having my program mysteriously corrupt memory at run-time.
- Rust has very nice support for portable CLI applications, both in the standard library and in third-party libraries.
- With a bit of extra work, I can usually deliver a single, statically-linked Linux binary. Go is even better at this, but Rust does it well.
- It turns out the "algebraic data types", what Rust calls "enum" and "match", are just a really nice way to write down every possible "case" or "state" of some value. If a piece of software involves hundreds of special cases, Rust allows me to think about them clearly.
- Rust has surprisingly good third-party libraries for many things. (Not everything.)
- Multithreaded Rust apps are a dream.
Cons:
- Rust forces me to keep track of whether things live on the heap or the stack, whether I'm passing them by value or reference, and so on. This is good when I want performance, but for other kinds of code, it's just a "cognitive tax."
- Rust's learning curve is higher than Go, but arguably lower than C++. This is especially true if you've either never worked with stack/heap/pointers before.
- Rust tends to favor "mostly functional" architectures over "mutable objet soup" architectures. This pushes you to use less-familiar designs for games and GUI libraries, for example.
> Rust's learning curve is higher than Go, but arguably lower than C++. This is especially true if you've either never worked with stack/heap/pointers before.
I tried to learn Rust when I only knew Python and had not CS education, I gave up rather quickly.
Then I did some projects in C and later on C++, and now I understand Rust.
Because I understand what are the problems it tries to solve. Imho only after you have some skills in C++, and/or some kind of CS education, can you really appreciate what Rust is about.
> Rust tends to favor "mostly functional" architectures over "mutable objet soup" architectures. This pushes you to use less-familiar designs for games and GUI libraries, for example.
One example would be the lack of pragmatic rust native GUI libraries. There are wrappers for GTK and Qt, which are probably your best option.
The most popular pure rust ones are based on somewhat esoteric patterns and seem prone to being abandoned.
Within that there are some interesting options, like for functionally reactive programming and immediate mode guis, but these paradigms aren’t all encompassing and in fact cover a pretty minority use case based on what GUIs are being created today.
Maybe one day we’ll all only ever create purely functional GUIs and speak Esperanto, but until then, we’re a little hamstrung in rust.
I think that's more a function of the newness of the language than anything else. Golang is (I say this as a Rust developer) more popular, has been around longer, yet still has the same dearth of GUI libs.
I can't speak for Go, but idiomatic Rust is not friendly towards the kinds of object graphs that are typical in traditional GUI frameworks with event handlers etc. You either end up with ARC everywhere, which is a pain because it's not transparent in Rust; or you have to come up with the aforementioned "exotic patterns", which significantly raises the bar for adoption.
Yet, web developers have been using some of those exotic patterns for ages and they don't complain. They even prefer that way to traditional GUIs by using Electron massively now. Rust GUI frameworks are typically based on strict separation between a model and a view and communication between them with messages which is something much closer to web programming than traditional GUI programming. An OOP soup with arbitrary handlers messing around with state directly in random places is IMHO a bad idea in any language anyway.
MVC originated in desktop GUIs, originally for Smalltalk. Java Swing was, I believe, the first mainstream GUI framework which used it thoroughly. Most GUI frameworks since then are also MVC (or some derivative thereof, like MVVM in .NET). This is just a way of organizing code and has little to do with presence of absence of event handlers, or the web.
It's a big piece of the learning curve, and one that usually only shows up when you try to write larger programs. Plus there's very little the compiler can do to point you in the right direction when your object soup needs major refactoring. From what I've seen, a lot of folks either give up on Rust or start writing blatantly unsound unsafe code when they hit this.
On the upside, a lot of C++ game programming uses an ECS-style architecture, which does work well in Rust.
When I tried to be more functional, it was really awkward in Rust
I wanted to write some helper functions that applied an argument to a closure and I got this code
fn apply<A, B, C, G>(mut f: impl FnMut(B) -> G, a: A) -> impl FnMut(&B) -> C
// must still be `for<'r> impl FnMut(&'r B) -> C`, because that’s what filter requires
where
G: FnMut(A) -> C,
B: Copy, // for dereferencing
A: Clone,
{
move |b| f(*b)(a.clone()) // this must do any bridging necessary to satisfy the requirements
}
needless to say, Rust doesn't do automatic currying or any advanced functional language tricks so it's just painful to write the same type of code
Yeah, your functional programming skills will probably be most useful at the achitectural level in Rust. Because mutation is very local in Rust, your individual functions can often get away with being imperative, but your overall architecture can't really rely on unrestrained mutation.
But if we zoom in from the architecture to the individual lines of code, Rust is more of a mix of functional and imperative styles. Rust's iterators can be fairly pleasant for working with lazy streams. Closures are more of a mixed bag. This is partly because closures need to worry about ownership, which complicates things by splitting closures up into Fn, FnOnce and FnMut. And each closure is a separate anonymous type. So, yeah, Rust supports quite a few useful things with closures. But as your example shows, the type declarations can quickly become intolerable.
So my strategy is to go with the flow, and keep things simple. I only bring out the heavy type declarations on special occasions, when they provide a big payoff. Otherwise I keep things as simple and boring and concrete as I can.
There are a few popular Rust libraries which make very heavy use of generics. I find that this sometimes backfires, making those libraries slower to compile and harder for me to understand.
* "Safe by default" (you have to use 'unsafe' to turn off the safety features), unlike C++ which is "unsafe by default" (v[i] is undefined behaviour if i is out of bounds, for all of C arrays, std::arrays and std::vectors).
* Really easy to do multithreading -- I had reached the point with C and C++ where I was of the opinion it is "almost impossible" to write large thread-safe programs, and all communication should be between processes with pipes, wherever reasonable. In Rust I again feel I can safely and productively write multi-threaded code.
The things Rust doesn't let you do (throw pointers around everywhere) can get a little annoying, but I find the extra safety increases my productivity enough that it's worth the tradeoff.
You didn't say otherwise, but this is such a frequent misunderstanding from people who don't write Rust, or write only safe Rust (which is fine) that it's worth pointing out the unsafe keyword and Rust's choice to be "safe by default" are not directly related. Culturally obviously they come from the same place, but for example:
v[i] is bounds checked in Rust even in unsafe. The unsafe "super powers" don't include "magically now indexing has no bounds checks" but because Rust's get function v.get_unchecked(i) is marked unsafe‡ you would need to mark code unsafe to use that function and that function doesn't have bounds checks.
If you write:
unsafe {
println!("{}", v[i]);
}
The compiler will point out that unsafe isn't doing anything for you here, v[i] is safe, marking it unsafe is just a waste of everybody's time.
C++ doesn't have bounds checked array access for its abandoned built-in arrays, but it does (these days) have bounds checked access for std::array and std::vector as a separate member function, however because they aren't the default few C++ programmers use them even where they undoubtedly should. This is an ergonomics issue, it's just easier to write needlessly unsafe C++.
‡ This said just get() before I edited it, but Steve corrected me, see below.
C++ has had std::vector::at() - which checks for out-of-bounds indices - since C++98. The reason why it's not used all that much in practice is because indexing is relatively rare - most of the time, you just iterate over collections, or otherwise use iterators rather than raw indices. And there's no ability to opt into bounds checking for C++ iterators in the standard - implementations usually provide some kind of checked iterators for debug builds, but performance is such that you'd never want that enabled for release.
This is also true in Rust for the same reason, arguably more true since Rust's built-in array type knows how big it is - but the observation in both languages programmers who want indexing write v[i] because that's the natural way to express what you meant, what's different isn't the frequency of need, only the language defaults.
> performance is such that you'd never want that enabled for release.
Let's make sure it's correct before we start worrying about performance
If you mean _ITERATOR_DEBUG_LEVEL=1, they're about 4x slower for iteration. I suppose "good enough" is subjective, but if this much is acceptable, I'd rather use C# - it's in the same ballpark.
Kind of true, except operator[]() does indeed do bounds checking across all major compilers when compiling in debug mode, and all of them have switches to keep the same behavior in release mode as well.
I work on a project that's basically a mini-OS designed to host WebAssembly applications.
The OS parts need to run on bare metal and do all sorts of cursed shenanigans, where Rust's separation of safe and unsafe code lets us better focus on proving to ourselves that the unsafe bits are working as intended (we also test our unsafe code under Rust's Miri interpreter/sanitizer for extra confidence). IOW, the killer feature is low-level control plus a reasonable expectation of safety.
The actual WebAssembly interpreter is also written in Rust, which is a good fit because inefficiency in an interpreter leads to inefficiency in anything running in the interpreter. The killer feature here is performance plus safety.
The overall package is bundled up into a CLI app that lets you deploy an instance of our OS with your application running within. Rust has a fairly nice wealth of libraries that make this pleasant, from CLI argument parsers to cryptography to OIDC and beyond. And it all gets packaged up into a single binary for easy distribution (even the OS and interpreter parts, which get embedded directly in the binary via `include_bytes!`). The killer feature here is great platform support (including cross-compilation) and small binaries (though you may reasonably disagree on "small"; altogether the binary (again, including the OS and interpreter) comes out to 25 MB).
Finally, Rust's lack of a pervasive runtime and great WebAssembly tooling means that it's easy to compile Rust applications to run on top of all this as well.
Keep in mind that I'm using "OS" here loosely; it's a bare-metal program that exposes a subset of Linux syscalls so that the interpreter (which itself is compiled for Linux) can run in the extremely specific context that we target. It's still early days so I won't spend too much time shilling it, but all the code is open source and lives under the umbrella of the Linux Foundation: https://enarx.dev/
Rust has replaced Python for me in my personal CLI tools where I expect to use them as an ongoing basis. At this stage I am almost as fast writing Rust as Python, find the Rust projects much easier in terms of environment setup than the whole pipenv/poetry/pyenv world, and enjoy the instant response of not having to wait for a python interpreter startup.
I also have some side projects like a gameboy emulator, where it's nice to have what is effectively a lower level language with the safety and features of higher level language.
My day job is mostly Java/Javascript though, rewriting the world isn't yet worth it, but there's certainly one or two of our systems where if a rewrite in another language was on the table I'd make the case for Rust
The "One Simple Trick" that lets you port most Python code straight to Rust is to take all the aliasing "object soup" references that Rust doesn't like, shove those objects in a Vec or a HashMap, and use indexes/keys instead of Rust references. This does require plumbing the Vec/HashMap around through different functions, but if you do it this way from the beginning it's not much extra work.
At that point, what's the benefit of using Rust over basically any other high-level language that has ADTs and pattern matching? Since you lose all the benefits of lifetime tracking that are unique to Rust.
> you lose all the benefits of lifetime tracking that are unique to Rust
I think I understand what you're referring to, but stop me if I get this wrong. If you put a bunch of objects in a Vec and refer to them by index, you have to be careful with operations like .insert() and .remove() that shift Vec elements around and change their indexes. Also if you make each element an Option<T> to support deletion without .remove(), you still have to be careful about indexes of deleted objects, because you might put a new object in the same spot later. I have several thoughts about this:
- If "memory safety without garbage collection" is one of the unique features of Rust, you still have that. A reused Vec index in safe Rust code will never corrupt memory or trigger a segfault or anything like that. It's strictly a "logic bug".
- These logic bugs only apply specifically to the objects you put in a Vec and track by index. Creating a Vec<Foo> and getting an index mixed up doesn't change anything about Rust handles your local variable of type Bar (or for that matter, your local variable of type Foo).
- These bugs are understandable for beginners, and you can follow what's going on with print statements. Contrast that with a dangling pointer into a C++ std::vector that just reallocated, where printing the freed object often appears to show good data, and explaining the bug means talking about malloc/new and the heap. Relatedly, unwrapping a None in Rust will always panic and will never coincidentally appear to work. (Your comment was comparing Rust to higher level languages, but I've heard similar comparisons to C++.)
- There are good options for fixing the problem. Beginners might prefer to put their objects in a HashMap with an incrementing key. There's a performance cost to that, but it's familiar and relatively footgun-free. More advanced folks might reach for something like "generational indexes into a slab", which gives you back some performance in exchange for making you learn a bunch of new buzzwords and figure out which implementation to choose from crates.io.
So yes, with those caveats, putting objects into a Vec does turn some compile-time bugs into runtime bugs. That is a downside, much like Rc/Arc/RefCell/Mutex come with runtime downsides. But I've heard folks describe it in terms like "turning off the borrow checker", and I don't think that's a very accurate way of describing it.
As you say, I was specifically comparing to high-level memory-safe languages, not C++. You can't get a dangling reference in Java or C#. Not only that, but you can't get into a situation where some reference points to one object at some point of time, and to another object at a different time - but you can with indices.
The reason why I describe it as "turning off the borrow checker" is because that's exactly what it is - those indices are pointers semantically, but there's no ownership tracking for them. So for them, you turned off the checker. The more you use them, the less checked your code is. If you use this approach in some isolated piece of code, it's one thing. But if it's the go-to solution, then it's reasonable to wonder why you'd do that instead of using a language that has built-in ergonomic safe references with no borrow checking.
Regular Vec indices yes, but not incrementing HashMap keys or other fancier things, if we set aside overflow issues.
> But if it's the go-to solution
Oh yeah, I should clarify this point. Rust definitely prefers to use simple ownership (i.e. the ownership/reference graph is a tree) wherever possible. As an example, say we've got a Python program with Person objects and Dog objects. Each Person has a `pets` collection that might contain some dogs, and each Dog has an `owner` field that points back to their Person. When we port this program from Python to Rust, Rust isn't going to be happy with the circular relationships between these types, and trying to implement `pets` or `owner` with references probably won't compile.
In cases like this, the most ideal, most idiomatic, go-to option is to break the cycle and try to achieve simple ownership. In this case, that would probably mean making the `pets` collection hold Dogs by value, and removing the `owner` field entirely. Any Dog methods that previously referenced `owner` would need a short-lived reference passed in as an extra argument now, or maybe we could change some of them into Person methods. If we can express our program in this style, that's almost certainly what we want to do.
But there are lots of programs where this doesn't work, at least not everywhere. Maybe a Dog can have multiple owners. Maybe a Dog can have no owner at all. Maybe Dogs want to track their relationships with other Dogs. If people and dogs are independent entities walking around in a game world, or if they represent rows from a couple of tables in some relational database, we probably have lots of problems like this. This is where we start reaching for patterns like Rc<RefCell>/Arc<Mutex> or indexes pointing into Vecs and HashMaps. (I think it's interesting that those Vecs and HashMaps look a lot like db tables.)
A point I want to emphasize here, though, is that even when the most important relationships in our program use these patterns, the majority of our object relationships are probably still simple. If each Person has a `name`, that's still an owned string. If they have an `age`, that's still a regular integer. When our program reads config values from the filesystem, all of our file handles and protocol data still follow simple ownership rules, use destructors for cleanup, and definitely don't get aliased anywhere.
In contrast, if we port our program to Java (or keep it in Python), we can't statically guarantee any simple ownership. Any time we pass a Person or a Dog or a HashMap or a byte buffer to some function, we might worry about that function retaining a reference to it, and we might start making defensive copies or reaching for immutable types. We can still use lots of simple ownership, and for most of our objects we probably do, but we've lost the benefit of a compiler that can check that for us.
Kind of a tangent: When we make aliasing mistakes -- which we can do in Rust in these fancy arrangements, or in Java/Python whenever our data is mutable -- that usually leads to "spooky action at a distance" bugs. Some operation on `foo` has mysterious side effects on `bar`, etc. We've all been there. But I think where these bugs graduate from "annoying" to "insanity-inducing", is when multiple threads get involved. That's when we really want the option of simple, statically-checked ownership, and that's where I think Rust-isms like "Mutex owns the data it protects" are a big improvement over the tools we had before.
Logic bugs of this sort are way worse (arguably) than some memory corruption errors you might see in C. The latter can be caught by a very manual usage of valgrind and the litany of similar too, while in the former’s case, you have to find that you even have an error, and track it down.
Sure, a beginner may not know about these tools, but my point is about the manuality if fixing the problems, one almost doesn’t need any thinking .
I do agree with that point as far as it goes. There are definitely cases (like single-player games) where logic bugs like these suck up more developer time than memory corruption bugs. But I'd push back in a couple ways:
- Much of the time, maybe even most of the time, memory corruption means security vulnerabilities. Logic bugs can be security bugs too, of course, but at least we can reason about them in local terms like "Is this particular part of the program security-sensitive?" Memory corruption doesn't admit the same kind of local reasoning. For any program that touches input from the internet, that's a huge deal.
- Just like C programmers can reach for Valgrind, Rust programmers can reach for design patterns that are more robust than Vec<T>. In this case, HashMap<u64, T> with an incrementing key is a very robust pattern. In practice (until you overflow that u64), that pattern catches 100% of your use-after-free-style mistakes. And I think a major advantage of Rust's approach here compared to Valgrind/ASan, is that it runs in production, so it works even if your test coverage isn't amazing.
For me the killer feature of Rust is its nice broad scope. I've done:
- embedded programs on avr hardware, which is impressive that something like rust can compile down to an 8 bit micro. Doing some sort of crazy map/iterator filter closures and finding the resulting assembly being nice and tight makes me super happy.
- web servers and clients (a scraper that collects scores from a web page and serves its own page of aggregated scores to my family).
- a low level opengl tmux/terminal client (Rust's Enums were a killer feature here)
- a windows GUI program for installing a PC game mod (99% developed on a mac and cross-compiled)
- a cli app to "compile" svgs down to png sprites for a web based game
- a launcher for Emacs that uses native cocoa apis to display a dialog on error
Each of these cases ended up pushing different parts of Rust for me. I would say the killer feature that spans all of those is Cargo and the crates.io registry. There are a ton of very good libraries out there—it feels like the heyday of CPAN or RubyGems to me.
I also quite enjoy how many programming errors Rust finds at compile time. With Rust code I spend way more time fixing compiler errors, but when it compiles I typically don't find a ton of major logic errors. I personally find it extremely rewarding when I get something to compile and then it just works. This happens more in Rust than in other languages I've heavily used.
I've written no code professionally in Rust, but a fair amount of personal project code. I've written C++ both professionally and as a hobby. At least for hobby use, comparing the two, Rust is wonderful.
- It's easy to set up cross compilation.
- Cargo makes dependencies easy.
- Building is almost always just `cargo build` except for very exceptional projects, same with `cargo test`.
- `cargo fmt` makes formatting easy and built in.
- Clippy gives actually useful lints and can automate most refactorings.
- Rust-analyzer is fast to index decently large projects, works with almost all language features (even complex ones like proc macros), and has some real time-saver automation built in.
Working with the borrow checker can feel like a pain, but in many cases, feels like a fun puzzle (writing hobby code, I'm not under time pressure, so it's 'solve a puzzle' and not 'oh fuck deadline coming up').
But really, the tooling of the language is where it shines.
Phenomenal performance, drop-in concurrency support (`iter()` -> `par_iter()` with Rayon), and a great ML-based type system that makes writing correct code easy.
It also has some of the best developer UX/tooling out of any language I've used.
I'm building an HTTP CRUD app on top of PostgreSQL. I have previously used Go for various CRUD apps. The experience is better except for compile time. My favorites:
1. Much more expressive type system: no more `interface{}`.
2. Algebraic data type with `enum`. Exhaustive check.
3. `serde` crate is lightyears ahead of Go's `json:"wtf"`.
4. Compile-time checks against DB with `sqlx` crate.
5. Logging is a breeze with `tracing::instrument`.
6. Using macros to reduce boilerplate has better UX than Go codegen.
What these give me is more confidence in development/refactoring because the compiler can guide me most of the way.
Low lights:
1. Build time.
2. Async is still clunky. e.g. Try to implement an `actix-web` extractor or middleware.
3. Please just stablize nightly rustfmt features already.
> Do you find the 'stubbornness' of the compiler frustrating at all?
I actually love it. The more work I can offload to compiler the better. One simple example that frustrated me in Go was adding a field to a struct. You add the field the the whole thing still compiles even though the zero-initialized value probably broke your app logic. In Rust if I add a field to a struct, the compiler warns me about all the places that I need to double check.
> Would you mind sharing some of your experiences?
I highly recommend zero2prod book which is well-written, practical, but still teaches the essential principles (https://www.zero2prod.com/). You basically deploy a CRUD app to DigitalOcean from scratch. The best way to ramp up IMHO.
> How do you find the tooling?
Cargo is sweet. rust-analyzer is all I need. I need less extraneous tooling to be effective. For example, in Go I might use a task runner to watch the repo and run tests when I change a file. But in Rust I can just follow the rust-analyzer highlights and manually compile less-frequently.
> Also, I've considered Diesel for ORM, so was wondering if you've been using that too.
I was not happy with GORM (https://gorm.io/index.html) and never had a satisfactory experience with any ORM. I'm a fan of writing plain SQL, even in Go. It's just that with Rust sqlx I can get compile-time checks against the schema. It's not anything new (see Haskell), but it tightens the feedback loop and I have full control of the performance.
Thanks for getting back. I'll take a look at those resources you recommended. Certainly, working with Rust, a lot just 'feels right', it brings together elements I liked from other programming languages like Perl, Erlang, C. Those being: expressiveness, efficiency, a reasonably sane standard library, and functional goodies like immutability and closures.
> You add the field the the whole thing still compiles even though the zero-initialized value probably broke your app logic.
Ah, I found Python notorious for the same reason.
I've found ASP.Net's ORM to be quite good, though this is only with a year's experience, so perhaps I'm missing some cracks that might emerge later.
do you have experience with protobuf in rust? that's the main thing making me think twice about using rust in backend (protobuf doesn't have an one-true rust library)
I've built the most popular[1] actively maintained tiling window manager for Windows that is available today.[2]
It would have been impossible for me to build this in any other language. I knew absolutely 0 about developing for Windows when I started this, but I quickly realized that the Win32 API is a very old beast with a lot of footguns that Rust saves me from on a daily basis.
On top of that, with parking_lot I can quickly and easily detect incredibly rare instances of deadlocks, and in general have to worry very little about concurrency. All of this allows me to stop worrying about the noise and focus on the real work of how the window manager should behave.
Cloudflare is using Rust for network stacks, proxies, caches, compression, and image processing. Basically everything that you would use C or C++ for, but where security is also incredibly important.
NIFs for particular casting of numbers to other binary formats in erlang. NIFS for embedded database and data format in erlang.
Main reason ? It works. The compiler has real error messages that are helpful making the onboarding of anyone having to fix problems a far far better situation than any of the competitors. It has a proper modern toolchain.
All of these would be killer features. All together ? No competition.
I also write cli tool and all kind of parsers for embedded language. The libraries here, in particular if you want good error messages and UX, are simply the best in all other environment i know of.
I'm building WebAssembly bindings to existing Rust libraries [0] and lower-dependency geospatial tools [1]. Rust makes it very easy to bind rust code to both WebAssembly and Python. And by avoiding some large C geospatial dependencies we can get reliable performance in both wasm and Python using the exact same codebase.
Oh, that’s really cool. Rust noob here, and I hadn't seen the tooling for building Python bindings. That looks like it could be a very powerful way to speed up your Python programs (much easier than the “just replace the slow bits in C” advice that was standard.)
- Easy to build small portable binaries (as a primary language feature).
- The type checker ensures type consistency when writing out to SQL tables (SQLite is loosely typed). Code that reads from the SQLite database implicitly benefits from Rusts strong type checks.
- Macros to convert structs to SQL insert/updates.
- Reduce the chance of errors at runtime.
- Leverage as much as SQLite's write throughput as possible.
- When converting Stripes Open API JSON spec into Rust code (using another Node.js program), the Rust type checker ensures I have a well formed HTTP client - the strict compiler makes it a good target for generated programs. Read more about this idea at (https://willcrichton.net/notes/rust-the-new-llvm/).
FWIW in recent SQLite you can turn that off using “STRICT” tables, however that means the available types are restricted to INT(EGER), REAL, TEXT, BLOB, or ANY.
ANY is also “Stricter” in strict tables: it does no conversion whatsoever, whereas in normal mode it’ll try to parse literals to numbers, and store a number in that case.
I'm building commercial hardware products using embedded Rust, I love it. I cant really imagine going back to C. I haven't dug too deep into the offerings with this update, but none of the standard library stuff is relevant for me
ZeroTier version 2 is being built in Rust, with the main reasons being safety (it's a network protocol!), easier cross platform compilation, easier dependency management, and an all around better language than C++. It should help us greatly boost developer velocity while also be much safer from a security point of view.
That will also make it more attractive to other developers using Rust who want to embed the SDK. Speaking of which, I've emailed ZeroTier a couple of times now about licensing the SDK (the first email was last week), and haven't yet heard back.
We are using Rust to build a fast log event database. The workload includes a lot of asynchronous networking I/O and parallelized CPU-intensive work, like indexing and SIMD-accelerated string search. There are great libraries for what we need. The performance and low memory footprint are quite useful.
I've built a p2p file transfer program [0]. All my development experience had been in Python, but I really wanted to try a statically typed, low level language, and decided to give rust a try.
It was hard to get going, and I still only know basics, but everything just works! Typing, borrow checker, the matched results, all of that makes code bullet proof.
I've done some hobby projects in Rust. One was a SIMD abstraction library, where you could write code that looks more or less like SIMD intrinsics, but it would either at compile time or runtime select the SIMD instruction set for the target cpu.
The traits/generics/macro system made that feasible, though messy. You can do similar things with templates in C++
I've written a small automatic file organiser in Rust that I run in a Docker container on my NAS. Docker reports that the container is using 1.5 MiB of memory, so that's pretty nice. Pretty sure I wouldn't have achieved that if I wrote it in Python like I normally do.
I do a lot of cryptography stuff in rust that is ported to various platforms and embedded in larger apps so I don't have to write the same logic over and over (desktop/mobile mainly).
I've used it to build two "bulletproof" services that are never intended to go down (the first catches webhook events and puts them to disk and should never go down; the second processes the on-disk files separately so that reloading the configuration can't cause us to miss webhook events). It is far nicer to have error handling as a visible part of the workflow so that you don't get "whoops, didn't consider that" situations (we had this constantly in the Python predecessor that had problems with "uh, you forgot to mention this might not be pure-ASCII, so I'm going to fall over now" problems as various fields popped up with Unicode over time).
Best parts IMO (not in any order and not exhaustive):
- Not having to consider exceptions (panics are still there, but are much closer to the "exceptional" behavior exceptions are/were meant to convey)
- Explicit error handling with (usually) good error types/representation (instead of the nebulous "`err` that is a string" I've seen in what little Golang I've done)
- Threading without worrying about race conditions
- Dependency management
- Data structure focused (instead of object focused)
- The built-in `#[test]` features are also great and make it easy to test internal code without exposing it in some strange way purely for testing purposes.
FWIW, I find that it also makes your C++ far better because now you can more easily "see" lifetime problems that people tend to write out of habit (`return stackstr.c_str();` is a…favorite that compilers now warn about).
I write video streaming software in Rust. Rust helps us make sure we've got concurrency correct, has a very solid type system to help us make sure we've got general application properties correct, and seriously reduces the space where we have to go looking for segfaults and suchlike (primarily third-party C libraries and bindings to them).
I am currently writing a kinda complex GUI application intended to run on a browser. So it's in webassembly.
Compared to Javascript, Rust safety features allow me to iterate quickly and improves my productivity from "I won't be able to complete this project" to "it's trivial, just some work". Compared to Typescript, Rust gives me some performance and types that actually represent the data I use, but I am not sure if I lost a bit of productivity (the TS tooling is horrible, so programing a bit slower isn't the entire story).
I don't consider this a problem Rust is good for, but one that all the more suitable languages still don't support.
I use it to write backend web services. Some how it doesn't appear to be any less productive than typescript +nodejs combination. Library support used to be a mess but now a lot of good libraries exist to get the job done.
I'm a junior programmer who never went to college and writes Python for a living (ML researcher). I've been itching to learn another programming language, ideally something fast and low-level, and have had my eye on Rust now for some time. But in my brief experiments trying some of the docs' hello-world projects, I'm already finding myself a little out of my depth.
I'm realizing I don't know anything about compilers and very little about how languages work on a technical level -- stuff like "strong" vs. "weak" typing, memory management, stacks and heaps and garbage collection -- I've seen all those terms before but have no intuition for what they mean. I never have to think about these concepts because Python is such a high-level "it just works" scripting language.
Could anyone recommend some online resources for learning about the computer science of programming for someone with little prior experience? I'm not quite sure where to even start, but I'm eager to learn because at the moment I don't really feel like a "real programmer", more like a script kiddie. Heck, I only learned to use Git properly like 6 months ago!
CS50X is a great starting point as they talk about the intro into bits/heap/stack etc. In the first half they have the work using C which will show you how to think about all this.
I'd suggest doing the C parts and stopping at the python side. Once you stop, step into Rust and you'll see how much easier it is to perform all the things you were doing in C.
Yes, it's inferring that `array` must be an `[i32; 5]` because there exists an impl of `PartialEq` for `[T; N]` against another `[T; N]`, so `N` in `::from_fn` must be 5.
It takes a function with signature f(usize) -> T. So |i| i gets inferred to be a function taking a usize, and returning a usize (since we just give the argument straight back). So it infers [usize; 5] in the end.
For example core::array::from_fn(|i| i) == [0i32] wouldn't even compile.
You are right. The parameter given to the closure is the array index, which is usize. Since that's returned straight back the returned value is also of type usize, and the integers in the array in the assert are inferred to also be usize.
Which is in a way an even stronger showcase of Rust's type inference: the length of the variable is inferred from the constant array, but the type of the content of the constant array is inferred by the type of the content of the variable, which is inferred from the type of the closure.
Good catch. The parent comment is interested in how the array length is inferred though, so I'm going to consider the explanation close enough (that it's an i32 or usize isn't particularly relevant).
It's not anything specific to arrays, it's just how type inference works. Here's another example:
let mut x = HashMap::new();
x.insert(1, 2);
Note that nowhere did we need to specify the concrete type of the HashMap; the compiler sees that you eventually insert numbers into the map so it uses that information to fill in the generic type parameters for the key and value.
I think the one here is legit (perhaps more realistic would be where you're passing the new array to an actual function that requires a certain array length).
People always complain about Rust's type system being verbose- well, this makes it less verbose. It knows what you need from the function call, so it doesn't make you say it. And as an added benefit, if the required type ever changes in a way that can still be generated by the provided code (eg. the function you're passing to needs a different length), you won't need to update any explicit type annotations at the call-site.
If that's ever undesirable - you want to be very precise about your code - well, then you can do that too by specifying the generic parameters explicitly.
That isn't really inferring the size of the array though. [T; N]::map(impl FnMut(T) -> U) defines its return type as [U; N]. N is fixed at the original array literal.
It's inferring it from the array literal in the first place, but it is a different kind of inference from the original one. But I think that's what the question was asking for:
> What's a sane/legit use case of such inferring in general?
You general type inference, or inferring the array length? The latter follows from the former, so it would be kind of a weird hole in the language if type inference worked everywhere except array lengths.
The tooling ecosystem, for instance rust-analyzer, make this not an issue if you're reading the code in an environment that supports it. I have rust-analyzer to display inferred types when I hold `control+command`.
In other languages, this can be a brittle/expensive operation due to meh tooling. But with Rust, it tends to "just work".
Online viewers of source e.g. Github and such don't have all this information exposed yet, but they're incrementally getting there. For instance, "go to definition" works for Rust code in Github.
One could imagine a generic file format included as an artifact in source control, that online viewers could consume, to annotate spans of source code. Go to definition, show expanded type, etc.
In practice, type inference works best when paired with editor integration. Most languages/editors will give you hover-overs with inferred types, but some like rust-analyzer have started going further (the greyed-out text here is all inlaid by the editor): https://user-images.githubusercontent.com/3971413/96668192-1...
OK this is just kinda funny - I was right, but only by accident. For some reason I thought that the literal defaulted to usize not i32. The fact that this path makes my assertion of usize correct is pretty cool though. Thanks for diving into it!
Yes, I realize that I may have read these words slightly differently than the parent meant them! I just said the same thing in a reply to your sibling.
It's a dream once you understand it. You don't need to understand the math, just the general concept of type unification, and that type inference can flow in "both directions", as opposed to in e.g. C++ where `auto` inference only works in one direction.
No, it’s just the right amount, and makes perfect sense. This is the whole point of type inference: where there are multiple possibilities, determine types based on how you use them, and if you still have ambiguity after that, complain.
If you tried comparing it to something that wasn’t an array, or if you tried comparing it to multiple different-sized arrays, it would complain.
Sure, it's a lot of assumption, but the type system would prevent an invalid assumption from compiling (arrays are typed [T; N] where T is the type they hold and N is the number of elements), and you can always override it with a turbofish.
Awesome update! It's good to see scoped threads here. Btw, can someone explain how these are safe when the initial implementation wasn't? The problem was that someone could put a reference to a local variable inside an `Rc` or `Arc` and leak it with reference cycles, right? How is that case prevented now?
The original implementation gave out a handle that waited for all the threads to end when it was dropped (i.e. went out of scope). That would prevent premature cleanup. But it was possible to leak the handle so that it would never be dropped even at the end of the block, and then it wouldn't wait for the threads.
The new implementation calls a closure and waits for the threads when that closure returns. Unlike the destructor here's no way to stop that code from running when it should.
Side note: since memory leaks were deemed safe, there are easier ways to deliberately leak than using reference cycles. The most straightforward are `mem::forget` and `Box::leak`.
Wouldn’t you want to use static instead of const, otherwise you might get a new unique mutex at each usage? I think there’s a Clippy lint for that.
The playground rebuilds overnight (roughly after the nightly is released); releases don’t have as fixed timing so I tend to trigger a rebuild when I notice. This HN post was delivered before the git tag was published even, so I’ve started it.
Yes. If I say FOUR is a const Mutex with a 4 in it, whenever I talk about FOUR I get a completely new unlocked Mutex with a 4 in it, unrelated to any other Mutex regardless of whether it was made the same way.
This might be what you want if you, for some reason, need to make different Mutexes with a 4 in them all the time. But, if you actually meant one Mutex, which intially has a 4 in it, then that's a static variable not a constant.
Change FOUR to be static instead of const, and now when we print y each time it's gone up by ten because we're using the same Mutex which is what we presumably intended.
I think that is less about the general allocator API than it is about the fallible allocation for collections RFC ( https://github.com/rust-lang/rust/issues/48043 ), which is something that the Rust-in-Linux folks have been pushing for recently.
This is a huge update! At least, for me, I can see several things on my code base that could be changed because of this. (The first one is the lazy initialization of sync primitives; and the second one is BorrowedFd/OwnedFd.
Nope, the threads run in parallel. The variable x is mutably borrowed only once by the second thread. The variable a is immutably borrowed twice, so there is no issue.
If one thread tries to mutate the variable a instead of just reading from it, the compiler will output a compile-time error saying you can't do that. (But, if you wrap a into a mutex and add the required methods to lock the mutex, you suddenly can)
For those missing the context; this guy wants new Rust software on Debian stable’s old Rust toolchain. And complains about it in every Rust thread on HN.
A similar problem afflicts other software in Debian stable; Rust is not unique. The reasonable solution is to stop using Debian stable if you want updates.
Debian packages rustc-mozilla for building Firefox. Firefox ESR 102 will need Rust 1.59.0, and that version has already been uploaded to stable-proposed-updates. 1.59 is not that old (it's from February 2022), and this actually builds after installing rustc-mozilla (with cargo even):
I guess for the same reason, there's now an LLVM and Clang 13 build in Debian stable, too.
I'm not entirely happy how Mozilla forced Debian's hand here. But it means that there is a relatively current, Debian-blessed Rust compiler, and you can use it for your own builds if you are willing to upgrade from time to time.
To clarify, without weighing in on the merits of his complaint, what he is saying is that he wants to use Debian Stable's toolchain and is complaining that the ecosystem (crates on cargo.io) move to use the latest features so quickly, and don't have stable backport releases, which makes this difficult to do.
I think you're agreeing with me, but framing it as disagreement.
> the ecosystem (crates on cargo.io) move to use the latest features so quickly
They use the new features in new software -- implying he wants to use that new software. If he uses the old versions that predate the new features, this wouldn't be a problem for him.
The Debian example you're referring to was back from the release of 11. It's rustc was only 3 months old and already couldn't compile. That's not a "Debian is old" problem and it was only one instance. I've run into the same problem on other OSes as well. No OS hosted repo can keep up with rust and it's particular selection of bleeding edge types.
And it's hardly every Rust thread. There are far too many rust threads to even count, let alone comment on.
If your distro is unwilling to update a package with a single backwards-compatible minor version bump every 6 weeks, that’s a cultural issue, not a technical one. The “stability“ Debian imagines it gets from shipping a years-old compiler version instead of keeping up-to-date with non-breaking compiler updates is entirely mythical.
Oh, okay, we'll just write off the entire concept of release based distributions because of one start-up programming language then? Everyone switches to rolling release? That's pretty absurd.
`rustup` is awesome for me as a developer. It is not convenient if I am a sysadmin that wants everything to be stable and consistent: I can not afford to use special version/package manager for every single language. It is fair to complain that rust (or python, or julia, or any other language with built-in environment/package/version manager) do not play nice with OS package managers. OS package managers are starting to take "vendoring" a bit more seriously because of this.
you want stability (aka reproducibility across a fleet of workstations/servers), but also security updates too, right? but what do you mean by consistency?
and you don't want to create & distribute a new OS image for every security update, right?
... anyway, the whole problem is that only upstream can provide these kind of security updates. it's a big (and very very convenient) lie what stable distros are doing, but many times packages have to work their asses off to maintain this illusion. (when they have to patch/backport/hack packages.)
Rust as language and compiler provides the tools for this (stability guarantee, editions, etc), but the various devs that provide Rust crates might not.
sure, if Rust would artificially hold back new features that would definitely provide this "stability", but it's no wonder devs adopt new features fast. because they want them, it makes their life easier, and it usually makes users happier too.
what the Rust ecosystem is doing (and NodeJS and TypeScript and quite a few high-velocity newish ones) is providing updates to users faster.
of course there are some users that might not care about this, hence debian oldstable, and so on.
I get where you're coming from but Rust is a bad example of your point. Its tooling is straightforward to install and use. I've bootstrapped several build and CI/CD servers and Node / Python have been infinitely more troublesome. I only had a problem with Rust once.
The "stable" OS packages are mostly marketing without much substance and while they're convenient for you they also kind of lie to you as well.
Debian does it the hard way, by making a Debian package for every crates-io dependency used and patching Rust programs to use Debian's forked deps instead. It's seems like a massive pain for them, and has zero value for Rust end users, since normal Cargo won't use any of this.
Rust often carries its own bugfixes to its LLVM version (before they're accepted upstream). Debian unbundles LLVM from Rust and makes Rust use Debian's LLVM, which creates a risk of undoing Rust's bugfixes and causing miscompilations.
I don't think anyone should be using Debian-packaged Rust. Debian's and Rust's policies are fundamentally incompatible. This results in Debian having a hopelessly outdated inferior package that is a pain for both Debian users and the rest of the Rust ecosystem.
At the most recent RustConf William Brown spoke about how OpenSUSE deals with Rust[1]. If I remember correctly from his talk they started doing something very similar to Debian and then eventually settled on "just use rustup". I wish I had taken better notes because there was more nuance to the history there but I think they'll release the videos of the talks soon.
We're both talking about the same elephant but we're obviously touching different ends of it. The end I'm touching smells really bad.
The fact that so many distros have to go extremely far out of their way, breaking their standard processes, shows that Rust is unique in it's tomfoolery. You guys are advocating for the tail to wag the dog.
or find ways to override and install latest, for example latest LLVM that would work on debian stable - https://apt.llvm.org/ (maybe similar for rust?). I've switched to this on my debian stable so that I can compile carbonlang (it needs llvm12, debian stable had only llvm11) - with above I've actually got llvm15, and then week ago switched to llvm16 (I can adjust that back to say llvm14 if I want).
My distro gets updated software when it's available; sounds like your beef is with your distro insisting that the entire world should work on its timelines instead of vice-versa.
Why are you here flaming the Rust devs for having the audacity to dare release a new version of their software, instead of talking to the packagers that distribute years-old versions?
> This contrasts with say, Bash developers [who] will write for what is actually available on the OSes people are running.
This is obvious, right? Rust is compiled with what you have on your machine, while Bash is interpreted and so depends on the capabilities on the target machine.
> Want to use your system repo rust toolchain instead?
If you want to use your system toolchain, just install your software from there, it will be compatible with the available toolchains, if it's a library, you don't even need the toolchain if its an application. The software is obviously also gonna be "outdated", because that's the point of Debian, stable software that has been well tested with the system.
> This is because Rust devs, for now, are the type that will immediately beginning putting the new code features
There is the concept of MSRV (Minimum Supported Rust Version) that a great fraction of the community follows. You just specify publicly the minimum version that your library supports and enforce it with CI. This is not as good as having an official standard (say, in Cargo.toml), but a lot better than just implicitly depending on what you think is currently available in bash for most users.
> without consideration that it's not available in any OS repository toolchains
Arch's package has been available on the same day in the last Rust's release. We'll probably have 1.63.0 available today or tomorrow.
Yeah, I was very excited when that was added. I prefer to use use the system Rust as well, and my primary complaint was the hassle of having to manually resolve dependency versions (for packages not in the OS package manager) because Cargo hasn't had great support for restricting packages to ones compatible with your version of Rust.
There will always be some issues with package maintainers using new features without realizing it, and/or forgetting to bump the minimum version. But now that there is a standard for how it should be done, I can submit patches to these projects to fix the problems when I encounter them.
Do be aware, though, that this is just a way to make builds fail fast if you have an older compiler. It doesn't put the compiler in a mode that makes it reject features introduced after that version. If projects want to maintain an MSRV policy, they should be using CI to confirm that they keep compatibility. This flag only exists for improved communication to downstreams.
Using nix I ended up realizing that I want my system to be almost empty, providing not much more than a shell to any project I'm working on.
Anything that's comes from my system is something that might bite anyone else or myself when using the project, and allow the classical "works on my machine" scenarios, luckily here at least the issue is that it won't build.
Ask your OS package maintainers to update their packages if you want a more modern toolkit. Or stick to the one your package maintainers decided on if you can live without those features.
I don't think a tool that receives regular updates should necessarily come from standard OS repos, they should be stable. Perhaps the Rust people could host their own repositories but the fact they don't do it suggests that it's not worth the hassle. This stuff is handed out for free, take it or leave it.
As Rust is open source software, you're free to grab each Rust release, package it up, and offer it as a package on your own repository, or you could probably pay someone to do it for you if you find the right people. I'd much prefer a repository as well, but I accept that the Rust devs don't work for me and I don't pay for any services that would entitle me to such requests.
Python is interpreted—you have to write code that works on all of your target platforms. Rust is compiled. You don’t have to care (much) about what is installed on your target platforms.
Not true for most of the big hitter crates. MSRV is a big thing and I never understood why, but it was recently explained to me it was so the old tool chains in distros could keep compiling, so the big crates tend to bump MSRV very slowly. Obviously, each project owner decides this on their own, so it isn't universally true, but is common practice.
Theres also talk of improving the situation, including
- Depencency MSRV errors explaining how to downgrade (merged)
- The dependency resolver skipping versions that need newer Rust. To have reasonable errors, this will require the new pubgrub resolver. The main question is opt-in vs opt-out
- Warnings when using functions newer than your MSRV
I wish they would stop adding features and just to lock down the language, and focus on more maintainability and use. Adding features is nice but its one of the biggest issues that slows down adoption. More people care about learning a language and being able to read code without trying to figure out what some latest feature does rather than some syntactical sugar.
Most of the features here (and in Rust updates in general these days) are just the loosening of constraints on what you could already do. If anything, these types of changes make the language conceptually simpler.
This gets pointed out by somebody in the comment section under every Rust update. The trope that Rust is getting harder to use is tired, and not really based in reality.
Different syntax = different ways of doing things = codebases that diverge in implementation. It doesn't matter if the new way doing things is conceptually simpler, you are now creating 2 ways to do things based on what version of the compiler you use, which people will have to know, therefore increasing complexity, not reducing it. I don't understand how people don't get this by now. Saying "oh just learn and use the latest version" is completely unrealistic guidance.
And while its fine for high level scripting languages like Python, because the whole idea is to abstract a lot of the functionality into the syntax, the issue with Rust is that its trying to be the next C and be applicable for writing critically important pieces of software like parts of the linux kernel. You absolutely must have a locked down language set for this. The reason C is still used for the kernel isn't because of circumstances, its specifically because there is very few ambiguities in the core language.
> you are now creating 2 ways to do things based on what version of the compiler you use
Which of the features in Rust 1.63 adds a divergent way of doing something you could already do?
> The reason C is still used for the kernel isn't because of circumstances, its specifically because there is very few ambiguities in the core language
What are some ambiguities in the core Rust language?
C was used because it existed 30 years ago, and is full of ambiguities (undefined behavior), and Rust curtails UB quite a bit by defining many semantics that are left unspecified in C.
You don't need a "locked down" language to build an OS. In fact Linux just upgraded the version of C they use to C11. Time always marches forward, my friend.
The first is a new library function that takes a closure as the parameter. This function has existed for as long as Rust has been 1.0 in the crossbeam library, it's just now also in std.
The second change is a new api type.
The third change is a library change to increase flexibility of that library.
The fourth is arguably a bugfix, where existing syntax could not be used in an edge case.
The fifth is unifying the compiler implementations so that some types of patterns now work in code using the oldest compatibility mode that previously only worked in more current modes.
Almost everything in Rust 1.63.0 appears to be an improvement in the standard library. Scoped threads no longer require a third-party library, some types support "const" better, and a bunch of existing types got some useful new methods. The only real language change here is the "impl / turbofish" one, which allows something that should work, but which wasn't allowed before.
I'm not sure it would be realistic to totally stop adding features to the standard library, or to stop making existing syntax work the way a Rust user would expect. I would hope that any actively maintained language does things like these.
Literally every single thing in the post is generalizing a feature that already exists. Don't you think removing these special cases makes the language simpler and easier to use?
You might have a point though in a few releases when GATs are stabilized. I'll keep an eye out for your username complaining about that release ;)
I wish they'd break their current backwards compat. promises to fix some accidental complexity that has been introduced. Let legacy langs provide this kind of maintainability
For example, it seems as though "Word".contains(char::is_ascii_lowercase) should work, because after all "Word".contains(char::is_lowercase) works and char::is_ascii_lowercase exists and does what you expect, the same but for ASCII only.
However, for compatibility reasons char::is_lowercase takes a char, while char::is_ascii_lowercase takes a reference to a char, and so you can't just pass it to contains() and will need to write a closure to pass to contains to do ASCII instead.
Writing the closure is ugly, but it's nowhere close to how awful a compatibility break would be so even a million things like this (and so far maybe I can think of a dozen) aren't worth it.
They have the Editions mechanism for unavoidable breaking changes, but their target market is people who care a whole lot about stability and maintainability, so it wouldn't make any sense for them to jettison that design goal.
Unfortunately the Editions system (as nice as it is) only works for the language, not for the standard or core libraries. It's possible to create such a system (similar to glibc symbol versioning), but AFIAK Rust doesn't currently have one.
It's a systems language so they're not gonna break BC. Although if there's a really great feature that comes out that requires BC breaking, then they should do so.
Now we just need to do it all over again once Polonius finally arrives. :P