What's New in Rust 1.56 and 1.57
Episode Page with Show NotesJon Gjengset: Hi Ben, how amazing to see you here again.
Ben Striegel: It’s been quite a journey, Jon.
Jon: Yeah, I’m so excited this time, because we get to do arguably four releases today. Because there’s 1.56, there’s the Rust 2021 edition, there’s Rust 1.56.1, and there’s Rust 1.57.
Ben: It’s going to be quite a day, that’s for sure.
Jon: Yeah. Or night. Is it night? I don’t know.
Ben: It’s really relative, right, we can’t assume whoever’s listening to this right now is actually, you know, asleep. Maybe they’re actually— our soothing voices might lull them off, and that’s kind of their one ritual. They can only sleep every three months.
Jon: That— I mean, there are probably people like that, I think there are probably people using our very soft and soothing voices to fall asleep.
Ben: Oh, man. ASMR episode suddenly.
Jon: I know, right? That’s really what this channel is all about, you know. Putting people to sleep.
Ben: That’s what every podcast eventually evolves towards.
Jon: Yeah, exactly. I think we should just dig right into 1.56. What do you think?
Ben: I’m into it. Let’s jump in.
Jon: So obviously the big headline feature of 1.56 is the Rust 2021 edition, which was planned and announced, I think back in May, and has sort of gone through all the procedures since, of figuring out what exactly should and shouldn’t be in the edition. Do you want to talk a little bit about what an edition is, just to sort of recap?
Ben: Yeah, it’s useful to kind of just re-emphasize what an edition is. It’s
kind of Rust’s solution for evolving a language, while being able to make quote-
unquote breaking changes without actually breaking anybody. And so the idea is
that users can opt-in to breaking changes. And also that the compiler can
perform most changes automatically, so that you— actually, whenever you’re
migrating, you don’t really need to. And also, whenever you get a new version of
the compiler, if you create a new project using cargo new
, it will
automatically opt you in to the new edition, so that new projects over time tend
to be on new editions. And so it’s a very, kind of like, gentle, gradual but
inexorable push towards having a language that is quote-unquote modern in terms
of correcting mistakes or making— giving itself wiggle room for future
development, which we’ll see here shortly. And the important thing to note is
that two crates on different editions can always interlink. You can always
interoperate. There’s no split in the ecosystem. Underneath every Rust codebase
or Rust compilation unit that you get from every library is all the same. They
can all talk to each other on the same compiler version. So it’s incredibly
useful, and it’s— once every three years or so, it seems to come, and this is
our new gift that we can now present to you.
Jon: And I guess, explicitly, also part of it is that there’s no sort of release train, really. It’s not like, we need to rush this to get it into the next edition, or if we miss this edition, we have to wait another three years. My understanding at least is that it’s more, when we feel like we need to do an edition, and it’s sort of the right time to do an edition, then we do one.
Ben: Yeah, I think there’s kind of a general cadence for like, every three years, but that’s not like— there’s nothing— that’s not set in stone. It’s totally up to the people making it, all the volunteers can decide and— we’ll see this edition actually compared to the previous one is a bit less impactful. There’s still plenty of great stuff, but we can expect over time that as Rust gets more mature, the things that people regret will, you know, be reduced, in terms of the things that— we’re not making more mistakes, hopefully, as we develop the language, and that, you know, the pace slows down. So eventually. maybe there won’t be any more editions. Who knows? But the option is always there and this time there’s some great stuff in it. Let’s start with—
Jon: And it seems to be working pretty well, too. So far.
Ben: Yeah, I mean, like, people— I think we want to see more languages doing
this. I think it’s going to be a big thing in the future. I don’t want to say
Rust is the first one to do this. I think that back in the 90’s there was some
Scheme variant that started doing this, in terms of like, you would opt into a
new version and it would enable new things. And obviously you talked about, like
use strict
from Perl, and later JavaScript, it’s kind of the same thing. We’re
opting into sort of a quote-unquote, better version of the language. But this is
the most formalized it’s been, in terms of any kind of— is Rust mainstream? I
want to say a mainstream language, but I’m not sure if Rust isn’t quite
mainstream yet. What do you think?
Jon: I think Rust is going mainstream, I think it’s right on that, on the curb.
All right, so let’s dig into the very first part of the new edition, at least as
mentioned in the release notes, which is disjoint captures in closures. Now,
this is something that’s sort of been fielded a little bit up and down. It’s
been available on nightly, I think for a while. And the very basic idea here is
that if you have a closure that uses a field of some struct, like let’s say you
have a struct x
and you want to access field y
inside of the closure. Then
the closure will only now capture a borrow of y
, or take y
by value,
whatever the closure needs, rather than bring all of x
with it.
And there are a couple of cases where this comes up. For me, I think the most
compelling one is if you have a— let’s say you have a method on a struct that
takes a mut self
, and inside of it you have a closure that maybe, I don’t
know, filters an iterator or something, so it has to consume, probably in a
read-only fashion, some field from the struct. But then you’re also in that same
method trying to mutate a different field of self
. In the past that wouldn’t
really be doable, because the closure would end up capturing all of self
in
order to reference that one field. And therefore you couldn’t mutably access the
field that you wanted to change. Or you could do it, but you would have to
introduce some intermediate variable outside of the closure, that captures only
that field by reference, and then move that reference into the closure, in order
for the borrow checker to sort of understand what you meant. Whereas now with
this partial capture, this will just work out of the box, which I think is a
really nice change.
Ben: Speaking of methods, we should mention too that there’s a common
frustration in Rust about the borrow checker, where if you have a method that
takes self
by reference, that might make it a bit more difficult to work with,
because it will borrow the entire structure. It’ll borrow all of self
, and not
just any fields you want to work with. This does not fix that problem.
Jon: Yeah, so the take here is— I guess we should link this in the show
notes. Niko Matsakis had this blog post on view types, which tries to
get at this kind of idea, of being able to write a method that takes self
but
only needs to have access to certain fields, so that you could call it from
another method that has self
but is already borrowing a disjoint set of
fields. Which is something that would be nice to get down the road, but as Ben
says, this is not— we’re not quite there yet, even though this is a quality of
life improvement, it’s tackling a slightly different problem.
Ben: Yeah, it is in the kind of vein of making the borrow checker more precise. And even though it’s not technically part of the borrow checker code base, any time that Rust creates an implicit reference, that’s still going to be— users will experience errors with the borrow checker if things don’t work out. So this is kind of like, you know, on the path of making the borrow checker a bit better in many cases. And previously you could work around this too, this disjoint capture issue, by manually taking a reference to whatever field you wanted in a new binding, and then closing over that binding in the closure. And this kind of just makes that pattern more implicit, or you know, not necessary any more. Less verbose. And that’s kind of the general pattern for, in C++, what you might call a capture clause. Where you can say, all the things I’m capturing and closing over in this closure I want to take this by reference and this by move, et cetera. Rust doesn’t have those; Rust just mostly infers everything that you want. And it mostly works out really good. Only in these kind of, like, edge cases does it not work out. So this just makes it a bit less necessary for Rust to even have capture clauses at all. But who knows if— what will happen in the future. There are still, again, like the view types kind of thing, are kind of like capture clauses, although not on closures, obviously on method fields, but we’ll see. Who knows? Someday in the future, maybe.
Jon: The next step is one that we’ve talked about before, which is the
implementation of IntoIterator
, the trait for arrays. And this is something
that you can already do in the current edition. In this— as in— sorry, as in I
guess the previous edition, so 2018. IntoIterator
can be used on arrays, but
it only works because of this little hack we talked about last time, and what’s
going to happen in the 2021 edition is that that hack will be removed and there
will just be a straight up implementation of IntoIterator
for arrays, that
will iterate over them by value and not by reference. So it’s really just sort
of tidying up that, shall we call it, oddity from the previous edition, where
this kind of worked and and also kind of didn’t.
Ben: Yeah, we spoke at length about this in the previous podcast, and we
won’t go over it too much. I think it is worth pointing it out as kind of a
feature of the editions where— so I think people might misunderstand the edition
system, where you might think, oh, the old edition doesn’t get new things, it’s
kind of like, you know, you might imagine it’s like being like an old version of
the compiler, where you just stop getting new things. That’s not it at all,
actually. So old editions still get all the new stuff that they can, which is
pretty much everything. There’s only a few things that old editions can’t
generally get. And so in this case around 52, sometime earlier this year, I
think it was that version, somewhere around there. Every edition of Rust got the
ability to use into_iter
on arrays. In most contexts. There was only one
context where you couldn’t, and it was a pretty important context, which is in
the for
loops.
And now the new edition has, uniformly everywhere across the board, the ability
to use IntoIterator
on arrays. And so, old editions are kind of— they’re
featured, their full featured mostly, without— if a bit inconsistent, whereas
the new edition is totally consistent in its ability to use this IntoIterator
on arrays. And so it’s kind of an example of how you don’t need to lock people
on old editions out of getting new things, you can still give them new things,
even if nice isn’t— you know, the most nice or consistent form of those things.
Jon: The next one, I thought, is worth a little bit more discussion. So this
is around or-patterns and macro_rules
. So if you write a macro_rules
defined
macro, so a declarative macro in your code, and one of the arguments you try to
take is using the fragment specifier type :pat
for pattern. It used to be in
past editions that this would not match or-patterns, so if you have like
A | B
. Whereas in this new edition, they will match A | B
. And the reason
for this was that in the old editions you could only use A | B
at the top
level of a pattern. So if you write match foo
, in that first, like, the top
level of the arm, you could say Some(1) | Some(2)
, but you couldn’t in the
past write Some(1 | 2)
, where the 1
or 2
is inside of the Some
. And
therefore it wasn’t right for the macro pattern type to include ors, because it
would mean that— the user might give you a pattern that contains an or, but the
macro might put it in a place where it sort of ends up being a sub-pattern where
or isn’t allowed. But now, because Rust, since I think 1.53, now supports or
inside of nested patterns as well, it was realized in the 2021 edition, we
should just make it so that the :pat
fragment specifier type in macros just
includes ors as well. So that all macros that that mention these patterns allow
users to use the full power of pattern matching. And then there’s sort of a
specific version of it called, I think “pat name”, is that what they used? Let
me double-check— :pat_param
, it’s called. That matches only the part without
ors. And so this is one example where, if you move your crate from 2018 to 2021
you want to make sure that any macro rules you have that allow patterns as
arguments, that you are actually okay with the patterns including ors. Because
in the 2021 edition they will now do so by default. Which is almost always what
you want.
Another change in the 2021 edition is that the Cargo feature resolver now defaults to version 2 of the resolver. We talked about version 2 of the resolver a while back, so this was in, I think Rust 1.51, that we talked about how Cargo now has this new feature resolver, and the new feature resolver tries to ensure that crates only get compiled with the features that they need, in the closure that they need.
Ben: The closure that they need?
Jon: Sorry, in the dependency closure that they need. So what I’m thinking
of is something like dev-dependencies
and build-dependencies
for example,
which are— in the past, Cargo would unify the features across dependency between
the two, even though that’s not necessarily what you want. The new resolver will
make sure to only use the features that are declared in the— in a given
dependency block. So like, for your normal dependencies, your dev dependencies,
your build dependencies. The feature sets will be kept different for
dependencies, even though they happen to be used in multiple of them, which
might mean that the dependency gets compiled more than once. But it also means
that you only get the features that you actually asked for in that closure.
Which has some implications for things like embedded workloads, where something
might not even compile under a given target, but it would work fine as a build
dependency. For example, if a feature was enabled. We talked about this at
length back in the 1.51 episode, so I recommend going back and reading that if
you’re— or listening to that, if you’re curious about it. But that’s at least
now on by default, in Rust 2021.
Ben: Up next, there are some new additions to the prelude. So in Rust there
is a thing called the prelude. It is essentially— imagine you’re writing a Rust
program and ever wondered where, say, Vec
comes from, or Option
comes from.
They’re available to you, you can write fn main
, you know,
let v = Vec::new()
, or you can say, you know, let x = Some(...)
, et cetera,
and it just works, you don’t need to have a use
statement saying, you know,
use std::
et cetera, Vec
or Option
. And that all is provided by something
called the prelude. And it is just a few handpicked extremely essential items:
traits, functions, macros, and types that want to be available by default. And
adding things to the prelude, usually it doesn’t take much ceremony. There are
already namespacing rules in Rust that make it so that if, say, someone adds a
thing to the prelude, and it clashes with a thing that you already defined in
your own crate, then your own crate just wins. There’s no real problem there.
The problem with adding traits specifically though, is that it might introduce a
method on a trait that clashes with one of your own methods on a similar named
trait. And then that might make it so that calling a method might become
ambiguous, which might stop code from compiling. You wouldn’t get like a
confusing method thing, it wouldn’t, you know, at runtime, it wouldn’t, you
know, pick one at random, it would just say, hey, I can’t compile any more, I’m
not sure what you mean here, please use the extremely verbose explicit syntax to
tell me which trait method you want to call in this case. And so that means that
to add a trait to the prelude, you do need to do it over an edition. And so, the
specific traits that have been added in the new edition are TryInto
,
TryFrom
, and FromIterator
. Jon, do you want to talk about those first two?
Jon: Sure. So TryInto
and TryFrom
are, as their name implies, fallible
versions of the Into
and From
trait. The idea here being that there are some
types that can sometimes be converted into other types, but sometimes that
conversion might fail. The obvious example here is parsing, right? So if you
have a string, you can maybe turn it into a number, if it happens to contain a
valid number, but it might also fail. You can think of TryInto
and TryFrom
as being the generic versions of something like the FromStr
trait, which takes
a string and— or takes any type that can be converted from a string fallibly.
And TryFrom
and TryInto
were stabilized a long time ago, but it used to be
that you explicitly bring them into scope in order to use them, which meant that
people just didn’t use them that much, or didn’t even know that they existed.
But now that they’re in the prelude, it’ll be a lot easier to just like type
.try_into()
or .try_from()
, usually combined with a question mark operator,
to do these conversions very straightforwardly.
Ben: As for the last one there, FromIterator
, it is one half of a pair. So
IntoIterator
is the other side of FromIterator
. IntoIterator
is the trait
that controls how for
loops work. And so if your type implements
IntoIterator
, then you can use it with for loops natively. FromIterator
is
kind of the opposite, where mostly you would only encounter FromIterator
as
part of the collect
method. So if you have an iterator chain of things, and
then you call collect()
on the end, under the hood that is using
FromIterator
to do all the magic. And collect
(unintelligible— 18:51) very
often and so, but you don’t need FromIterator
in scope just to collect things.
It’s because collect
is already a method on iterators, which is already in
scope, the Iterator
trait. In the prelude, I mean.
And so, you might ask stuff like, why is it necessary? Well, I guess, you know,
we could say it’s kind of just for neatness because IntoIterator
is already in
the prelude, although a bit more concrete use case is, if you’re mapping over
something, and you want to say, you know, I want to map over this thing, that’s
an iterator, you can just map over, say, like the first— using first class
functions you can say, you know, Vec::from_iter()
as opposed to having to say,
you know, make a closure x
and then call x.collect()
on that closure. So
that’s a bit nicer.
Jon: Yeah, that’s true. The next one is something that I know you’ve been a
little bit involved with, and I think we should combine the next two, which is
that panic!
, macros now always expect format strings, just like println!
.
And that there is now reserved syntax for custom identifiers before the pound
symbol. So think of something like raw strings like r#
and then double quoted
strings. Identifiers followed— or immediately preceding a string; think of
something like byte strings. So if you type b
and then double quotes what you
get back is a u8
slice. And same thing for identifier preceding a character
literal. So this would be something like b'x'
, which gives you a u8
instead
of a character. Do you want to talk about these two and then how they interact?
Ben: Yeah, I’ll talk about this. So let’s back up real quick. There is
something coming up in an upcoming version of Rust, which you can preview right
now. It is called implicit formatting captures. So the idea is, currently, if
you’re writing a println!
macro and you want to, just like, you have a string
"hello"
and a string foo
and you want to put them together, you might type
println!
and then you have to give your thing in quotes, your format
specifier— your format string, really. And so it would be "{},{}"
, and then
you after this— it’s hard to code in voice, actually.
Jon: Yeah, you’re right. You’re right.
Ben: The point is that in your println!
, after you had your string, of how
you want to print it, you would need to say your two variables, like maybe you
have the string "hello"
, and a variable foo
and the string "world"
and
variable bar
you would have to end the println!
invocation with , foo, bar
and that tells the println!
macro what to put in your brackets individually.
And this is kind of a shorthand in fact. And so I think people are probably
familiar with the brackets in the formatting strings syntax, where you can put,
like, :
, and you can put {:?}
or you can put {:#?}
or if you’re formatting
integers or floats, there’s all kinds of various ways you can say that I want
to, you know, add some white space here, I want you to pad this thing, I want
you to round this thing to this many decimal points, et cetera.
So there’s a whole language, the format strings but people don’t realize that
that colon that starts all of these implications actually is a separator. And
before the colon, you can put exactly which variable you want to capture. You
know, to format in that one bracket. And so— but, implicitly it just, you know,
it’s an index counting from 0 to 1. And it will say, the first one goes in the
first bracket you find, the second one goes in the second bracket that you find,
and you could explicitly put 0
or 1
, or you can put— you could print the
same variable four times. Put 0
, 0
, 0
, 0
inside the brackets, and that’s
how it’ll work.
Alternatively, even more obscurely, in this syntax, you can give each individual
variable to print its own little ID. So in my before-mentioned println!
invocation, I could say x = foo, y = bar
at the end of the println!
, macro
and then I could use x
and y
inside the braces to refer to the individual
variables to be printed. And it’s a bit verbose, which is why it doesn’t really
get used very often. If you’re doing really involved format strings, it could be
useful.
But this explanation is useful to kind of— sorry, this explanation is useful to
demonstrate what’s actually happening here under the hood now, with this new
feature called implicit formatting captures. Which is nowadays, on current
nightly, it’s got merged, like last week or so. If you just say println!
and
then like "{x}"
and that’s the entire invocation, it will try to find a
variable called x
in your scope, and if it finds it, it’ll print that. And so
nowadays, if you’re like, you know, used to format strings from other languages,
the formatting facilities from, say, Python or JavaScript. This is how it
normally works. So you don’t need to, you know, redundantly say the name of the
variable you want to format. You just mention the name inside the format string
and then the language will find it for you. and that’s how it will work in Rust
in the future, where it will implicitly, if there’s no reference to, hey, I have
no idea what x
is, it will reach out in this running scope and find x
, and
then insert that. And so—
Jon: It’s going to be absolutely magical.
Ben: It’s going to be extremely magical. People have wanted this for a long
time, kind of like, because Rust is like, it’s extremely explicit, but maybe a
bit too much— Rust kind of always errs on the side of being explicit. And people
are kind of like, well, this is kind of, maybe a bridge too far, and it is, kind
of, naturally how macros work. But println!
itself is not the most— not the
least magical macro, let’s say that. It’s kind of involved. And so this is kind
of, the minimum that you could do, to kind of like, approximate the format
string facilities from— usually dynamic languages do this. So like, you know,
JavaScript, Python, which many people are familiar with. And so, that will
appeal to people used to that.
But there is a bit further yet you could go with this. And now I’m going to kind
of get into the realm of speculation. Because this reserved syntax for this
feature, now in Rust 2021, this would allow you to put any kind of arbitrarily—
it would allow the language developers to put any arbitrary identifier in front
of strings or character— string literals, character literals, et cetera. And
this would allow them to consider adding, say, like an f
specifier. Where as
Jon was mentioning, you can put, today, you can put like an r
for a raw string
or a b
for a byte string. Nowadays those are no longer special cased, and now
every possible letter or combination of letters is reserved for future language
development. And so you can imagine one day maybe there would be, say, like an
f
string, to kind of match Python 3’s f-strings syntax, where you can just say
f
and then quotation mark, and then you could say whatever and then close
quotation mark, and it’ll just automagically create a formatted thing for you.
And obviously this is Rust so it can’t really— you don’t necessarily want, say,
to allocate, like many strings would do, it would be a bit more involved than
that. It would probably create an arguments type, which is kind of an internal
type from the format module in std. And so there’s a lot of details and kinks to
work out before any of this could happen, but this reservation just makes it
possible to consider someday having this feature.
Still, I would say there’s still a lot to consider, especially with regard to
the actual syntax, because in Python, one of the things is that you can write
any expression inside the format string, you could like— you could do
calculations, you could call methods and functions, and anything you want. And
whereas the newly stabilized implicit captures just lets you write identifiers.
You can’t even do— there’s no field accesses, there’s no, like indexing allowed.
It’s just identifiers. And so it remains to be seen whether or not people want
to extend that to full expressions or some subset of expressions. These are all
things that would need to be figured out before f-strings could even be
considered. And so it’ll be a while before you, kind of get feedback from users
on how they’re using it. Like we’ll see whether people are eager to move their
println!
statements over to format— the new format implicit captures, and
their feedback on whether or not it’s sufficient for their use cases. So we’ll
see. So this this syntax kind of just opens the door for this in the future. And
the reason that it had to be an edition is because of, macros can kind of parse
Rust these days, kind of like limited ways, and so theoretically this could
break some macros. So that’s why it had to be an edition.
And then as for the panic!
change, that kind of comes along with this. The
idea of this new implicit formatting feature is that it should work on anything
under the hood that uses the format_args!
macro, which is what println!
uses
and assert_eq!
underneath uses, all kinds of macros use this. And so if you
have a macro today that uses internally this, then you will get this feature for
free, whenever this stabilizes in like two or three releases. But panic!
was
kind of— this was an oversight back at 1.0. A single-argument panic!
had a
different kind of a code path than usual. It didn’t go through format_args
machinery, it kind of just did a thing. And so it was a unique little snowflake.
And so that has been melted, and now it has been made more consistent. But
because that could break things, it’s only more consistent in the new edition.
Jon: Yeah, I’m so excited to see this, like making progress towards landing.
I thought some of the other proposals for reserved syntax, what it could be used
for, is nice too. Like you could imagine having, like, k#keyword
allow you to
create identifiers by the same name as a keyword in Rust. Stuff like that I
think could be neat, too.
Ben: Yeah, that would be useful for— so I think in the previous edition, in
the 2018 edition for example, one of the edition changes was to reserve various
keywords, and many keywords were reserved in advance of them actually being
used, kind of just like, you know, aspirationally, like async
and kind of
thing. So if you, say, had the ability to put k#
, what you do now, with this
new reserved syntax before an identifier, it would allow you to quote-unquote
reserve identifiers, without the need for an edition. So it makes it easier to
prototype, and to potentially get things in the hands of users, and then an
edition would then be able to go through and you know, get rid of that k#
on
front of it. So kind of a— you can imagine it’s kind of a beta version of
identifiers. So that’s kind of, one of the proposals. There’s plenty of things
that we’re could talk about here. I wrote the RFC for this, so I could go on for
for ages about the things that we could do here. But they’re all very
provisional. I don’t want to get people’s hopes up, because really it’s just—
Jon: Well, you have my hopes up now, so—
Ben: Oh no!
Jon: So now you’re going to disappoint me, Ben.
Ben: Inevitably, like I always do.
Jon: The last one is— it’s not a big change, but it is one that’s worth
noting as far as what editions are for, which is there are two warnings that
have been promoted to errors in this edition. Bare trait objects, so this is
something like, if you have a Box<MyTrait>
, that is no longer legal. That now
has to be Box<dyn MyTrait>
. And there are lots of reasons why this became a
warning in the first place, but the reason it’s becoming an error now— and the
same is true for the other warning, ellipsis_inclusive_range_patterns
, which
is the ...
syntax for inclusive ranges, which has been deprecated for a while
and should be replaced with ..=
. Both of these warnings have existed for a
long time, and are really intended to— or when they initially landed, were
intended to signal, this will be removed in the future. And we just want to give
people, like, as long as we can, to sort of adapt to this change. And because it
would be a breaking change to make them errors within an edition, we use the
edition mechanism to also have these kind of deprecations actually take effect.
Because the underlying machinery is the same, right? If you write
Box<MyTrait>
, it can compile to the same code as Box<dyn MyTrait>
. So it’s
only really the syntax that changes, and therefore we can turn it into an error
at an edition boundary, because the edition boundary is explicitly opt-in
anyway. And so this— I don’t think there are any warnings of this kind that are
specifically being proposed right now for the 2021 edition, but it’s more that
as we accumulate new warnings and deprecations and stuff, the edition boundary
allows us to actually do that hard deprecation as part of that move.
Ben: Yeah, the policy towards warnings is interesting, in terms of— I think it’s evolved a bit since the previous edition. So the idea of this edition has not actually made anything— I don’t think it’s added any new warnings of the same type, where it’s— they will eventually become errors. Rust already has a facility for turning a warning into an error, that isn’t on an edition. And sometimes this is required for— because the Rust stability policy still allows breakage in the cases of unsoundness, or like, just obvious wrongness. And obviously, even though it’s allowed, that doesn’t mean people want to cause that kind of disruption. And so the idea is to minimize disruption. And sometimes people will say, you know, use an edition, sometimes people say, just do it. And just doing it means using a facility called the future incompatibility warnings. And this is kind of a newer thing, where— imagine you’re writing some Rust code using a dependency. Dependencies might have warnings in them, but by default, if you’re compiling with Cargo, it won’t show you— it’ll just kind of squelch all the warnings that dependency will give you. Because it figures that you don’t actually have control of it, in terms of like, you don’t— modifying the code. So like, don’t worry about it, it isn’t your problem. But if, say, some warning became an error in the future, that would cause your code to stop compiling when you upgraded your compiler, which is your problem. And so the idea is that this class of warnings called future incompatibility warnings will kind of pierce the veil, and will be shown to you if you’re using a crate that has them. And so this is kind of a newer thing in Cargo. I’m not sure if it’s, like, really being used yet.
Jon: Yeah, I think we mentioned this briefly last time, too. Because that’s when they mentioned that future compatibility lints would be added— would start to be added, or start to be used.
Ben: Yeah. So this obviously gets used sparingly because of the potential breakage. And so Rust has very good infrastructure for detecting breakage, in terms of running tests against the entire library of code on crates.io, which is an incredibly involved process. It takes like, days and days, but it’s extremely good at finding a secret breakage from any feature, and it gets run all the time on every Rust release. So it— ideally it would only be used for things that would have almost no impact, and that would, you know, have very big, like, security impacts, say like, you know, if there’s some kind of a unsoundness in something. But it’s kind of, I think subsumed the idea of doing these kind of warnings to errors in editions, unless they— obviously they could still do it if they wanted to. But I don’t recall that this edition has actually added any new warnings of this kind. And like I was saying before where, you know, over time, editions— we would expect them to become, you know, as the rough edges of Rust get smoothed over, fewer things need to be in those, maybe. It’s just that this time there was nothing that needed that kind of treatment.
Jon: Yeah, although I still feel like there’s a decent amount of good stuff that landed in this edition too. But I think you’re right that over time we should expect editions to get smaller in scope.
I want to mention one thing. So those are sort of the big changes in the
edition. But I want to mention the cargo fix
tool, which I think many people
don’t really know about or don’t really know what it does. So cargo fix
is a
great way to have Cargo update your code, when Cargo knows how it needs to be
updated. And in particular, it has a --edition
flag that you can pass to make
Cargo walk through your code base and make any changes that it can, like, do
automatically to move you to the new edition. So this would be stuff like, if it
realizes that you’re, like, manually including TryInto
for example. I don’t
know if that particular one is one that it supports, but it’s this kind of stuff
where cargo fix --edition
can just make any manual changes that you would be
required to, if you were to adopt the new edition. It’s a great tool that’s
worth knowing about, and looking into.
Ben: And that finishes off the edition-related things. You want to get into the new features on every edition, 1.56?
Jon: Always. I love these. So one big one is, Cargo now has a rust-version
field in the package section of Cargo.toml
, and this is something that’s sort
of been on the table for a while, and there’s been a lot of sort of bikeshedding
of what the name should be, but also some more fundamental questions about what
the semantics should be. The basic idea here is that you can declare in your
rust-version
field, the minimum supported Rust version for that crate, and the
reason why you want to do this is primarily to give your consumers better error
messages. So currently, if someone takes a dependency on you, and you make a
change that requires a newer version of the compiler, then what that dependent
crate will see if someone compiles it, is there will just be some hard to
understand compiler error. Like it might be some—
Ben: If they’re on an old version of the compiler.
Jon: If they use an old version of the compiler, exactly. Then they might
see something like, this function doesn’t exist, or this edition doesn’t exist,
or this feature isn’t stable, but has been stabilized in the version that you
were using when you published that dependency. Whereas with— if you add this
rust-version
declaration in your Cargo.toml
, then when you— or your crate
gets built using an older version of the compiler, that compiler can just say,
I’m not new enough to compile this crate, and therefore I need— you need to
update the compiler, in order to compile it. And that’s a much more helpful user
message because it actually tells them what went wrong and what version they
have to update to.
Now it’s worth noting that there are some caveats to this feature. The first and
most obvious one is that because support for rust-version
was only added in
Rust 1.56, there is no point setting rust-version
to a value lower than 1.56,
because a lower version wouldn’t know about the field in the first place.
The second caveat is that the rust-version
field does not feed into Cargo’s
dependency resolver. So if someone is using an old version of the compiler, and
they take a dependency on your crate that has declared a rust-version
, Cargo
won’t pick the latest version that’s still compatible with their compiler
version. It’ll just pick the latest version that matches the semantic versioning
specifier. And then if that doesn’t compile with their version, it will give
them an error. This may or may not change in the future. It’s a little unclear,
because we do want to incentivize people to upgrade, and we don’t want them to
sort of just transparently get an old version, and not know that they’re running
on an old version. So there’s some subtlety there.
There’s another third caveat with this, which is that— remember that if you
declare a rust-version
in your Cargo.toml
, but you have dependencies, then
that Rust version might actually be— might actually be invalidated by a change
to your dependencies. So imagine that you declare Rust version 1.56. and then
one of your dependencies suddenly requires a feature from Rust 1.57, and
upgrades their rust-version
. Then now your crate lists rust-version = 1.56
,
but actually requires Rust 1.57, because of a dependency. There’s not really a
good mechanism for enforcing something like this at the moment. Partially
because most crates don’t really stick to a given minimum versioning sort of
schedule. And this is a somewhat hard but also somewhat orthogonal question,
that this doesn’t try to tackle, this is mainly trying to get at, can we give
you better errors when a dependency requires a newer version of the compiler
than you have. There’s still unresolved issues, like the ones we’ve talked
about, but at least this is a step up in terms of the user experience here.
Ben: Yeah, I mean, so just as a notable example— so for example, the regex
crate from Andrew Gallant (BurntSushi), that supports all Rust versions of
1.41 or greater currently. So if you are— that’s kind of, like, one of the more
thoughtful versioning— so like, that crate kind of, like, sets the standard for,
does regex support it. In terms of like, how old of a compiler, Rust compiler,
does, like, a general well-developed, well-maintained crate support. So that was
released, what, June of last year, about, so it’s— you would take in the future
that kind of like, people have asked for, you know, long-term support releases
of Rust, and it’s kind of been thinking about, what version should we support,
and it always boils down to, like, what version shipped with Debian, et cetera,
and in various package managers. So it’s a— this kind of, is one step towards
figuring out, okay, like, what is our policy, what should a thoughtful crate do,
for supporting old versions of Rust. Because it is kind of hard, because it
means that you have to consciously not use features that are shiny and new. And
for the sake of your users who don’t want to upgrade, or can’t upgrade, and kind
of like, hold yourself back.
Jon: Yeah, it’s definitely, I think a part that the Rust ecosystem hasn’t really figured out yet. In part because it is challenging, once you take dependencies, because the dependencies are sort of out of your control. Unless you’re willing to, sort of, hold back your dependencies and say, I don’t depend on any 1.x version of my dependency, I depend on any version that’s, like, 1.x, but less than or equal to 1.5. Or whatever it might be. Right? You need to constrain them, because you don’t know whether future versions will conform to the same Rust version requirements that you promised to your consumers. So it’s a thorny situation, that I think we’re still navigating, but at least this provides a mechanism for declaring your intentions. Which is a step forward.
Ben: Let’s keep moving on. So an actual language feature this time. New
bindings in binding @ pattern
, where “at” is the @
sign. I think this is
kind of one of the more obscure corners of the pattern syntax, and this has
taken nothing from, just— it’s from ML, or Haskell, or some language that
supports patterns, it has for a million years, it’s kind of just one of those
things you don’t see very much in Rust. Maybe in those you see it more, but this
allows you to bind an identifier to some pattern that you’ve already matched.
And so this has been supported for a long time, but now you can have multiple
identifiers bound in the same pattern. Or like, it’s kind of complicated but,
just know that now— all of the restrictions have been removed in terms of, you
know, letting you write this identifier in various places. There are still some
things about, you know, the values must be Copy
, I think. In terms of, to
actually make it work, to avoid various ownership shenanigans, but it’s a pretty
cool feature. If you’re into deep patterns, if you’re really into, like, match
statements, learn what the @
symbol does, and see if you can use it in your
code.
Jon: Yeah, I’ve certainly found the @
symbol handy. There are very limited
cases where it comes in useful, but then it can really simplify your code
structure, and it it makes me happy that now it’s— you can use @
in more cases
to like, unlock that way of writing patterns.
Jon: I think we’re then onto stabilized APIs. So this time there weren’t too
many sort of large ones that stuck out to me, we got a bunch of shrink_to
methods. The idea here being that there’s already shrink_to_fit
for many
collection types, for example, that reduces the heap allocation used to store
something to the minimal size that it needs to be in order to contain its
elements. So remember how vectors for example, as you push to them when they
grow they usually double in capacity. But that means that after you, if you know
that you’re done pushing to them, you might then have all the spare capacity at
the end and you want to reduce your memory use, so you can use shrink_to_fit
.
There’s now shrink_to
, which takes an argument saying what capacity you want
to shrink to. And that seems like a nice sort of quality of life improvement for
those cases where you want finer control over how much memory you actually use.
And this does— this doesn’t actually truncate the thing, like— it’s sort of
bound by, or capped at the size— the length of the collection. But it is sort of
a— now you have this option as well, of shrinking to something that’s not just
the minimal fit.
We also got BufWriter::into_parts
, which is really nice if you’re using a
BufWriter
for making your writes to your, sort of, I/O writes more efficient,
then into_parts
actually lets you take a BufWriter
that you’ve written some
stuff into, and turn it back into the original writer that you had, and any
bytes that haven’t been flushed yet. Which is really nice if you’re writing sort
of low level I/O code. I think that’s all I really wanted to mention there.
We got some const
ifications though. We got a const
ification of
mem::transmute
. Do you want to talk a little bit about that, Ben?
Ben: I want to mention it, but also I want to ask you if you know, it’s—
okay. Obviously transmute
is kind of one of the granddaddies of unsafe
code,
in terms of what it allows you to do, is just totally break type safety if
you’re not careful. Which can itself trivially break memory safety. So you have
to be extremely careful using this function. But now it works in const
context. Or rather, it— before now, it worked only in const
and static
context. Now it works also in const fn
. And so this means that it’s possible
to, obviously, use it wrong, because it’s in unsafe
code. But do you know what
unsafe
code does at compile time, if you use it wrong, in terms of, like, what
does undefined behavior actually defined to do at compile time.
Jon: Yeah. So this is really interesting. Like, what does compile-time undefined behavior actually mean? And it is actually something that— we’re sort of still trying to figure out what the implications of that are. And I don’t think we have a great solution, or a great answer to that question yet. Apart from, if you have undefined behavior at compile time, it basically means your runtime code can end up being whatever. Like it’s not just that the runtime behavior can change from that point on, but you don’t even know that the code you end up with will actually be representative of the code that was written in the program. Like, you don’t know if the compilation will be correct.
Ben: Yeah, the code will still be wrong, obviously. Like, Rust doesn’t
magically fix it for you, or make it not undefined behavior. Or not totally
bananas. But I think that we should, you know, emphasize that it’s not going to,
like, corrupt your compiler process. That’s like, one of the things that they do
say about this, where it’s like, even though it’s undefined behavior, it’s not
going to make the compiler go astray, it’s not going to like, you know, it’s not
an attack vector on you yourself, if you’re compiling code, at const fn
, that
invokes some undefined behavior. And even in many cases, the expectation is that
it will be able to catch undefined behavior, and then issue an error. Although
it does not guarantee that. So the const
engine named Miri will— does do its
best to detect undefined behavior whenever it finds it. But obviously it can’t
guarantee it. If we can guarantee it, we wouldn’t need to worry about unsafe
blocks in the first place.
Jon: Yeah, it’s a really interesting, sort of, intermediate stage of undefined behavior, that I think currently you should think of as, if you invoke undefined behavior at compile time, what code actually ends up in your binary may not represent the code that’s in your code— in your source files. The two— just the moment you invoke undefined behavior, you’re sort of telling the compiler, you can optimize based on whatever assumptions you want, and I don’t care whether— or I don’t even now get the guarantee that the code is actually correct. Or accurate, maybe is a better representation of that.
Ben: One more library API thing to mention in this release is the
implementation of From
— so for all of the collections in std
, so HashMap
,
etc., they now implement From
an array of tuples, or From
an array, and
sometimes from an array of tuples for things that make sense like this. So
essentially if you’ve ever written a Vec
before, if you’ve ever made a Vec
before, you’ll be familiar with the vec!
macro. So like, just vec![1, 2, 3]
makes a Vec
of 1, 2, 3. And this is equivalent to Vec::from
and then the
argument to from
could be a fixed-size array of like, [1, 2, 3]
. So that—
both that Vec
macro and that From
implementation have kind of been exclusive
to Vec
for a long time, even though Vec
is only one of a few collections in
std
. And so people have asked for a long time for a nicer way to construct or
initialize many collections. And so for example, let’s just use HashMap
s.
Like, everything I say for, you know, HashMap
s will be true for everything
else, too. Where if you want to make a HashMap
these days, before this change
you would say let mut x = HashMap::new()
and then you would say, you know,
x.insert
, like 1, 2, 3, 4 etc. Actually, insert(1,2)
and then the next line
x.insert(3, 4)
, and so you need to have one insert
, like insert command for
every element that you wanted to have in this initialized HashMap
. There was
no real nice kind of like, one shot constructor for a HashMap
before now, and
people have, you know theorized, let’s just have like a hashmap!
, macro and
I’m sure you can find macros for this on crates.io. But there’s always the
question of, what should the syntax be? Because every other language has their
own syntax for, you know, hashmap constructors, and then people get bogged down
in bikeshedding forever. And it’s kind of just like, let’s just, for now,
instead of having to worry about syntax, let’s just give all these collections a
From
impl. And so the idea is, currently today you can already collect
into
these HashMap
s, collect
into a HashMap
from an array of tuples. And so in
this case it lets you just construct the HashMap
outright from an array. So
it’s kind of a nicer way— it’s kind of a half measure towards having a first
class constructor for HashMap
s and so now you would say, instead of having,
you know, let mut x = HashMap::new()
and then inserting various lines, you
would say let x = HashMap::from([(1, 2), (3, 4), (5, 6)])
. So it’s a fair bit
nicer. It’s one of those nice quality of life things. And that applies for every
collection these days, and doesn’t mean that there won’t be, maybe someday, some
kind of macro for making it nicer. But honestly, ever since—
Jon: This gets pretty close.
Ben: It gets pretty close, and ever since const generics became powerful
enough to have the From
impl on Vec
, like I mentioned before, I have
honestly been preferring that, I don’t know, I kind of, I don’t have anything
against macros, but I tend to gravitate towards functions whenever I don’t
strictly need a macro.
Jon: Yeah, they also get formatted more nicely by rustfmt
, in general.
Ben: Yeah and it’s also kind of just with IDs, it works a bit nicer to use methods instead of macros, in terms of like— easier for them to type check and present errors and that kind of thing, so—
Jon: That’s true. I had a couple of other things from the changelog too. So
one of them— and if you read through, this might— you might read this and go,
like, what does that even mean? Which is, there’s an entry there that says
“Remove P: Unpin
bound on impl Future for Pin
”. And this is a PR that I
filed, that I actually filed while writing Rust for Rustaceans, because
there’s a section in there where I talk about, like, Pin
, and why the various
bounds are, what Unpin
means. And then I got to this— the fact that there’s an
implementation of the Future
trait for the Pin
type. And there’s a bound on
there saying that P
— like, this impl Future for Pin<P>
had a bound that
said, where P: Unpin
and I just couldn’t articulate why that bound was
necessary when writing the book. I just, from all I knew about this stuff, like
that just didn’t make sense to me. And after thinking about it for a long time,
I came to the conclusion that this must not be necessary. And so I filed the PR,
and no one else could find that it was necessary either. And so now this bound
has been removed. It’s not— I don’t think anyone cares about this
implementation. It’s like, very much of a weird niche corner. But if you’re
really curious about Pin
and Unpin
, it’s one that I recommend you go look
at, and sort of look at the discussion and the documentation for why this is
correct, because it is an interesting insight into how these all pieces, these
weird pieces fit together.
Ben: On a personal note too, I do want to mention that that bullet point and
the previous one, the impl From
array for collections. Those were both hidden
away in the detailed release notes. They didn’t make it to the actual blog post.
And in fact, those two were written by yours truly, both— you wrote this one. I
wrote the previous one. So I want to say, I think we’re being— I think there’s
an agenda against us.
Jon: I think you’re right. I think that we’re—
Ben: They’re trying to silence us.
Jon: We’re not part of some kind of cabal that we should be part of.
Ben: The Rust illuminati.
Jon: Yeah, exactly. I want to be part of these backroom dealings for setting up the release notes.
There’s another really cool, like, low-level change that landed in 1.56, which
is an improvement to Instant
backsliding protection. And here we need to
rewind, to give a little bit of context. So the std::time::Instant
type in
Rust guarantees that it is monotonic. That is, if you take an Instant
, and
then later on in your code you take another Instant
, the duration between them
is always going to be positive. So it’s always going— the value underlying the
Instant
is always going to go up. And normally you would sort of assume that
this was guaranteed by whatever underlying mechanism the operating system or the
platform provided. But of course, many of them have bugs, and do not actually
guarantee this behavior. Like for example, on x86 systems on Windows, on I think
AArch64 systems, and a couple of others, like particular versions of the Linux
kernel on particular hardware. There’s, like, just a bunch of cases where this
is just not true. You can take an Instant
and then take another Instant
, and
the newer Instant
has a lower value. And for this reason, the Rust standard
library includes this thing called backsliding protection, which is basically:
if it detects that it has an implementation that can sort of slide backwards in
time, it forces the new value to be newer than the old one, so that code can
rely on this guarantee. And the way it does that used to be through a mutex, so
if it detected that the current platform has this kind of backsliding property,
then it would take an Instant
and then grab a mutex to check that the value
was newer than the last value it gave out. Of course, mutexes experience
contention, so this would cause some pretty serious performance degradations for
any application that just like, takes a bunch of Instant
s and looks at the
time. And so what landed in 1.56 was an improvement that moves this from being a
mutex to being an AtomicU64
, which has much less contention and doesn’t
actually block at any time. It doesn’t have the same kind of contention
experience. And so this is going to be potentially a pretty large performance
improvement for performance-sensitive applications that do have to deal with
time, which is basically all of them. Because they use it for things like
profiling and logging, like it comes up so much that I’m very excited to see
this land and I think there are probably production customers out there, that
are like, this is going to significantly matter to our use case.
Ben: One last thing from the detailed release notes here, is that the LLVM 13 upgrade happened in 1.56. And that mostly brought a bunch of more like, you know, things that are in the weeds we won’t talk about in terms of improvements, I’m sure there’s plenty of performance improvements and compile-time improvements. One of the bigger things that came with LLVM 13, though, is called the new pass manager. And this has the potential for a lot of— a lot better compile times for various LLVM projects. And the— it’s actually not enabled right now. Some bugs were discovered a bit after enabling it— it had to do with recursively— mutually recursive functions getting inlined exponentially. So that’s currently still being worked on in the LLVM side, but once it finally gets enabled, it should have a pretty noticeable performance improvement. And the pass manager essentially— just LLVM as a compiler, as a back end. It does a bunch of little passes for simplifying code in various ways. And one pass might happen, the next pass happens after the previous pass. And the new pass manager essentially just has better caching and lazy behavior— and we’ll link to a blog post from LLVM talking about the details, because it’s a bit over my head, essentially.
Jon: I have one very last bit too, which is, if you care about, like, build
systems or the configuration of Cargo. The Cargo configuration files— so whether
that’s in /home/.cargo/.config
or in .cargo/config
. Now has an environment
variable section, so you can have Cargo set environment variables for you, in
the configuration. This might be helpful for, like, setting OPENSSLDIR
or
something, everywhere in your project. Or in any Cargo project that you build,
if you have a particularly weird deployment setup. It’s just like a neat quality
of life improvement for the people who need to configure Cargo and don’t want to
have to do it all over the place, in lots of configuration files. That’s now
supported directly by Cargo.
I think with that we can move to 1.56.1, and this is, this was a security
release that came about from a CVE that spanned much beyond Rust. So this is
CVE-2021-42574. And this CVE was all about Unicode code points that affect
rendering of code. And it’s a problem that actually hit a lot of projects. Not
just related to Rust code, like, this affected Python, Node.js, Go, like
basically everywhere. And the basic vulnerability is that there are Unicode code
points that allow bidirectional overrides for text. So this is something where
you can put a sort of Unicode code point in a piece of, say, UTF-8 encoded text
that reverses the direction of text. And then you could do it again. And what
this allows is that if you have something that renders code, you can have the
code contain a Unicode code point that sort of, makes the next text be written
backwards over what was already written. And then you reverse direction again,
and you write some different text over it. And there’s some good examples in the
security advisory that we can put out, but this means that a malicious attacker
can, like, make code that looks like it says one thing when you look at it in an
editor, but the actual code when compiled and executed does something completely
different. And of course this is not actually related to Rust. This is more
about how code is rendered, and it might be rendered in a way where like, a
security audit of the code, if someone reads the code it looks fine, but what
actually gets compiled and runs is malicious somehow. But what the Rust project
did was, they implemented a lint in the compiler that is deny
by default, that
basically yells at you if any source file contains any of these potentially
problematic code points. Now, this doesn’t solve the problem, right? There’s
still— you can still just opt out of this lint entirely, for example. But what
it does do is at least give you sort of a warning sign if there is some code
that contains this. That doesn’t help you if the code is, for example, on
GitHub, and you’re just reading through it, which is why this CVE was so
widespread in the community. Like, GitHub as well posted that they have
addressed this vulnerability by adding a little warning flag at the top of all
source files shown on GitHub that that contain these code points. So it’s just
an interesting attack vector, where the Rust mitigation is really just giving
you a sort of lint for this, but it’s not really a Rust problem. But at least
the compiler can do what it— can do a little bit, to try to ease or mitigate the
problem.
Ben: And to emphasize too, the big concern is like, comments mostly. Because you need to be in a context where you can put arbitrary Unicode characters which don’t— doesn’t happen in many places. So comments are the big deal here. Although Rust does now support Unicode identifiers. And I’m not sure if— I think some of these characters are allowed in Unicode identifiers. Rust does not support the full gamut of Unicode as identifiers, unlike, say, Swift, you can’t do an emoji function name, that kind of thing. But Rust, ever since the original release of Unicode identifiers, it has always linted very aggressively against extremely strange things like this. So any kind of confusables, or mixed scripts. It has a bunch— there’s maybe, like, four at least, lints individually checking for this kind of thing. So I know people are always worried, and you should be, but Rust takes it pretty seriously.
Jon: Yeah, and I think the Unicode standard actually has a, sort of— a section on identifiers, and how you should vet identifiers, and how you should— what subset of the Unicode symbol table you should allow in identifiers. And I think Rust follows that very closely. I know there’s a lot of discussion when Unicode identifiers were stabilized, on what rules exactly should we follow. But I think Rust ends up doing something very sensible there, and that probably reduced the impact of this particular CVE on Rust code too.
Ben: All right, and let’s move on to our final of four releases today, Rust 1.57.
Jon: It’s very exciting. It’s also odd, you know, because it’s—
Ben: It is odd. Yeah, it’s extremely odd. Although next time, probably less odd, the next release. Also, it’s much less impactful than 56. We won’t put an entire hour talking just about this one.
Jon: Yeah, that’s that’s probably true. Although there are some really cool
changes in there. I’m excited to see this release too. So the first thing here
is panic!
in const
contexts. Do you want to talk a little bit about this,
Ben?
Ben: Yeah, sure. So the panic!
macro is now usable in const fn
. And so I
might wonder what it’s useful for. Well, and— it’s actually really great for
creating your own compiler errors. I’m pretty sure that’s— it’ll, like, just
hook into the compiler error machinery, and if you panic in a const
, you can
kind of do a custom compiler error. I’m not sure if it’s— I’m not sure if
that’s, like, the ideal way of doing that. There might be like a more, like, you
know, first class way of doing it, but it’s a quick and easy compiler error.
That’s the new hotness, and this also allows you to use the assert!
macro in
const
context, which is pretty important in general, I’d say more important
than panic!
for writing code. Although we should mention it does not yet allow
the assert_eq!
macro because assert_eq!
under the hood uses formatting code,
and the formatting code has not yet been made const
. And so— and in particular
the panic!
macro, that it works in const
context, only allows certain forms
that don’t include formatting.
Jon: Yeah, and this is also why unwrap
and expect
don’t work, right?
Ben: Yes. So working towards making const
code be pretty and equivalent to
non-const
code. Now, you know, unwrap
and inspect
are pretty important for
that, although we’re getting there.
Jon: Yeah, and I think maybe one way to frame this particular thing is that
there are a bunch of things that internally use panic!
that we would like to
make const
, and a sort of requirement for that is to make panic!
itself
usable in const
. This would be things like assert!
, which is now enabled,
assert_eq!
, which is not there yet. There’s also things like the todo!
macro, and the unreachable!
macro, which are also really just aliases for
panic!
.
Ben: I love todo!
by the way, just like, one of my favorite macros.
Jon: Yeah, it’s pretty great.
Ben: What a good macro. I love it so much.
Jon: The next change in 1.57 is support for custom profiles in Cargo. So you
might already know that Cargo has multiple compilation profiles. There are four
that sort of come standard. There’s dev
, release
, test
, and bench
. That
have slightly different configurations, like for example, dev
includes debug
info. release
includes more optimizations. test
includes the config test
,
among other things. And bench
is like test
, but with more optimizations. And
now you can declare your own profile. So for— the example they give in the
release notes is that you might have a production profile, that is like release
but it also uses link-time optimization to like, really squeeze out every last
inch of of performance from the binary. You can imagine having a profile that
includes things like profile-guided optimization, or you might have a profile
that is like release
mode but with debug info, or test
but with release
optimizations. There’s all sorts of interesting profiles you can imagine coming
up with here. And at least now you have the machinery for doing so rather than
having to, like, modify the pre-existing profiles, and being able to give them
better names.
Ben: Up next, we have fallible allocation. So these are library APIs which usually don’t get their own section in the release notes, but these ones are pretty important, because of what they kind of indicate for the broader direction, of Rust getting used, and that is Rust in the Linux kernel. And so the Linux kernel obviously, famously cares about not crashing. One of those things. And you might be wondering, well, what do these new APIs allow you to do? Why wouldn’t I use these all the time in my code? And they’re about memory allocation.
And so the idea is, very often when you write code, if you just say like— you
make a Vec
, say, if you’re, like, creating a Vec
, And you want to push to
it, that might involve reallocating the entire vector, which takes up more
memory. And what happens when there is no more memory? For many applications,
this question is entirely moot. The reason is, that on many OSes, Linux and
macOS both, the OS just lies to you about how much memory you have. It is
extremely common for the OS to say yes, I have memory, here you go, when in fact
there is no memory backing it up. And it will kind of just like, lazily give you
memory as you request it, or it’ll do shenanigans with like, trying to compress
memory if there is contention. Or they have too much stuff being given out. And
so generally if the OS lies to you, there’s really nothing you can do. And so
many applications— the Rust standard library just doesn’t, just doesn’t care,
really. And so it’s like we— it would be an extremely burdensome API
requirement, to require every single thing that could possibly fail to allocate
memory, to bubble that up to the user. And so if it happens, then we’ll just—
we’ll panic and— actually it’ll abort, I believe, if that happens.
But, if you are in an environment where you do— can guarantee that the OS won’t
lie to you, for example, if you’re writing an OS then it’s extremely useful to
know that you are out of memory. And so this is kind of, it is not sufficient
for Linux’s use case, I believe. I haven’t really read the proposal, but there
was a long discussion. I’m not sure if it was a full RFC, but there have been
many people involved with Linux, and contributing to the Linux kernel, have
talked about how, okay, we would like to see various things from the standard
library and Rust, to guarantee that we could use, even like, you know, libraries
from crates.io, and you know, guarantee that they’re not
going to allocate, you know, abort the process, you know, panic the kernel, just
because they failed to allocate some memory. So this is kind of, maybe step one
of that process. I’m sure there’s much more to go, but so essentially, Vec
,
String
, HashMap
, HashSet
, and VecDeque
all have try_reserve
as a new
function.
Jon: Yeah, and the big— one of the big missing parts here, I think, for the
kernel, is that they want to sort of statically guarantee, so guarantee at
compile time, that there’s nothing in there that could panic because of out-of-
memory, which— this is a step in the right direction, but it doesn’t solve the
problem, right? There is nothing that prevents someone from writing .push
in
the kernel, without first calling try_reserve
. So there’s still a question of,
how can we statically guarantee the things that the Linux kernel requires us to
guarantee. And that’s still being figured out. But at least now we have the
startings of the mechanisms that the kernel can use. And other use cases too,
like embedded use cases, can use when they do want to know whether an allocation
succeeded or failed.
Jon: We have a bunch of other stabilized APIs too, which are sort of less,
monumentus than try_reserve
. I think the first one I wanted to call out is,
Command
now— so this is std::process::Command
, the thing you use if you want
to spawn a process inside of a Rust program. So previously, it had, like— you do
Command::new
, you give a program name, and then you can add arguments, you can
set the current directory, you can set environment variables. But you didn’t
have a way to inspect a given Command
before you execute it, and look at what
arguments it is being passed, or what environment variables it is being passed.
Whereas now, it’s gained a bunch of get_
functions, so there’s get_program
,
get_args
, get_envs
, and get_current_dir
, which gives you the configuration
of a Command
and lets you introspect it before you choose to actually execute
the command.
The other one that I think is pretty neat is that there is now a map_while
method on Iterator
. So the idea here is that, it’s sort of like filter_map
on Option
. Or I guess not, there’s a filter_map
on Iterator
too, where it
will only— it will run each item through a closure, and the Iterator
will only
yield the items for which the closure returns Some
and not the ones where it
returns None
. With map_while
, it’s sort of similar, except that it will stop
the first time it gets None
. So it’s like “take until”
(editor’s note: should be take_while
),
and then with a
closure that when it returns None
, it stops taking. And the closure also gets
to map the item, so it’s like another nice utility on Iterator
that makes it
somewhat easier to write concise Iterator
sequences, especially if you’re sort
of trying to write your program in a somewhat functional style.
Ben: And const
ification continues unabated. This time with the
unreachable_unchecked
function. And so essentially, unreachable_unchecked
—
it’s an unsafe function. It’s a way of telling the Rust compiler, hey, here is a
branch that can never be taken. And so for example if you have an if
statement
where it’s just literally if true
, you could have an else branch with an
unsafe { unreachable_unchecked() }
in there, and then that would tell the
compiler, hey, this branch can’t ever be reached. And that’s kind of a useless
example, but there’s various examples where, for example, if Rust is requiring
you to write a branch for something, like say you have— you know that some— that
the value in an Option
is a Some
, you’ve already verified it to yourself,
maybe you, like, made it yourself earlier up in the function. But Rust doesn’t
know that. It just sees an Option
. And so maybe it forces you to have a branch
on there. If you want to use that branch and you wanted to put, you know,
unsafe { unreachable_unchecked() }
in there, that indicates to the optimizer
that that is a totally unreachable branch, and it should be optimized away. And
that means there won’t be any kind of branch there at all in the final binary.
Obviously it’s unsafe
, because you do have to manually ensure that you never
get there. Because if you get there, undefined behavior.
Jon: Yeah, because unlike unreachable!
, which panics, this does not
include any panic machinery in that branch.
Ben: And so yeah, so there’s the unreachable!
macro, which is just, you
know, it’s great for indicating this, but also still being safe. And now this is
a const
function. And so nicely, it will give you a compiler error. So that’s
actually, it won’t be undefined behavior at compile time. I’m pretty sure,
right? Yeah, yeah. So Miri is smart enough that it will try to take the branch,
and it will say, hey, it’s a compiler error. It’s— we’ve reached it. So it will
give you a nice compiler error saying, hey, you messed up.
Jon: Yeah, assuming that it is being evaluated in const
context, right? So
if you have a const fn
that includes this, then if that const fn
is called
from constant context, then hitting it will be a compiler error. But if you
invoke it at runtime context, then it will be undefined behavior.
Here, too, we’ve done our usual thing of walking through the changelogs. And I
think one that struck me as particularly entertaining was the fact that
Vec::leak
no longer allocates. Which— so you may be familiar with Box::leak
.
So Box::leak
takes a Box
, an owned Box
, and returns to you a 'static
mutable reference to the underlying memory. And the intention here is that once
you leak the value, it will never be dropped, and therefore the heap memory is
just valid forever. So it can give you a 'static
reference to it. And the same
thing exists for Vec
, but with Vec::leak
, what it would do is, it would
first shrink the allocation to be actually the size of the length of the array,
not just its capacity, and then give you back a reference to that slice. Of
course this meant that it was a little weird to call Vec::leak
, because it did
an allocation and a copy of the entire vector, in order to move it into the
smaller capacity. But now with this change, Vec::leak
will do the sort of
intuitive thing, of keeping the whole memory allocation and still just giving
you a reference to the slice of just the contents. Which I thought was
entertaining, that it didn’t do this in the past.
Ben: Similarly, the final item in my notes is that, as of the new release, Rust now has a new tier 3 target of the Nintendo 3DS.
Jon: It’s so great. I don’t know why— I don’t know why this was added, but it—
Ben: Somebody wanted to write Rust on the 3DS, and they were like, you know what, why not? Let’s do it. And so we should mention too, that the tier system— tier 1 are targets that get, like, you know, tested every single, like every single commit gets, like, the full test suite run on it. And so they’re very well tested. Tier 2 is— they are guaranteed to compile with every commit but they were— with every release, that is. But they won’t run the full test suite. And then tier 3 is anything goes. Like, we’ll add support for this if you really want us to, but it’s up to you to make sure it works. There’s no Nintendo 3DS test runner somewhere, in someone’s, like, kitchen, hooked up the internet, with, like, you know, running random Rust code every release to make sure it still works.
Jon: It’s very sad. We should definitely have a fleet of 3DSs.
Ben: We should.
Jon: I had a couple more things. So in Cargo— and this is actually a change
that happened in 1.55 but but didn’t make the release notes. Which is that Cargo
will no longer have RUSTFLAGS
be set for build scripts. It used to be that if
RUSTFLAGS
was set in the environment, and then Cargo would just pass that
through to build scripts. But it wasn’t entirely reliable, because if for
example RUSTFLAGS
was not set, but there were rustflags
set in the Cargo
configuration file through, like, build.rustflags
for example. Those would not
be set in RUSTFLAGS
. So build scripts that relied on the RUSTFLAGS
,
environment variable would actually get a sort of misleading value. Now, since
1.55, and announced now in 1.56, Cargo will set a different environment variable
called CARGO_ENCODED_RUSTFLAGS
, which is guaranteed to hold the current set of
RUSTFLAGS
that Cargo is using. Encoded in a way where, like, whitespace
splitting and such isn’t a problem, that build scripts can actually rely on.
Another change that I thought was kind of neat is that, there’s been a lot of
additions of the must_use
attribute to functions in the standard library. So I
think we mentioned must_use
back when it first stabilized. The basic idea is
that you add it to a function definition, and anyone who calls that function is
required to do something with the return value. The idea being that for example
if you have a function that has no side effects, it’s a pure function, then
calling it without looking at the return value is probably an error. An example
of this might be saturating_add
on integers, which returns the result of doing
the saturating add. It doesn’t actually modify the value that you call
saturating_add
on. And therefore not using the return value means that you’re
probably not doing what you think you’re doing. And so it has a must_use
attribution. And a bunch of similar attributes were added in 1.56. Like, on the
Stdin
and Stdout
and Stderr
locks, on thread::Builder
, on
Rc::downgrade
. There’s a whole host of them. And I think this is just a really
good way to just eliminate errors that are hard to spot when you just like, look
at the code briefly. And you really need to look at the signature. But here the
compiler can actually help you realize that this was wrong. I think there are
like 800 attributes that were added to the standard library in this— in 1.56,
which is wild. And really cool.
I have two more that are both fairly minor. One of them is that
File::read_to_end
and File::read_to_string
has got this really cool
optimization now, where previously if you had, like a vector, and then you
called— you open a File
, you did read_to_end
, read_to_end
would just like,
start streaming the file into the vector, and the vector would just like,
dutifully double its size every time it fills. But that means that you spend,
especially towards the end, you spend a lot of your time just reallocating the
vector and copying over all the bytes that you’ve previously read. And so
reading a file ends up being this, like, sort of exponential process of copying
bytes, in edition to actually reading from disk. And what changed in 1.56 is
that now, File
will first look at, like inspect the file system, and look at
the size of that file. And as long— if it can realize what the size of the file
is, it will pre-allocate, or sort of resize the vector to be that size before it
starts reading, which should potentially significantly speed up file reads,
which is really neat.
It’s also an indication that there are still pretty significant improvements that can be made to the standard library. So like, feel free to dig into there and just like find something interesting and start poking at it, if something doesn’t perform to your satisfaction.
I think that’s actually all— the other one is a smaller, I’ll mention this as
well, which is that if you have a macro invocation that uses curly braces, you
can now use, like .method()
or ?
after that invocation. Which is nice for
things like, if you’re using the quote!
macro, from the quote
crate, for
doing, like, procedural macros and code generation, then you can now use
quote! {
, write some sort of code that’s going to be expanded into a token
stream, }.into()
to turn it into a proc_macro::TokenStream
that Rust
expects. Previously that .into()
would fail to parse, for relatively silly
reasons, and that will now just work. It’s a big quality of life improvement for
just like, macros not being as weird.
Ben: That’s kind of an obscure feature that we could, like— as a gift for
people who actually listen to the entire podcast, any macro invocation can use
any bracket of, you know, parentheses, square brackets, curly brackets, and it’s
just not a big deal. Like you can have a println!
with square brackets if you
want, or with curly brackets, or anything. Only convention stops us from having,
you know, the vec!
macro with curly brackets. You can do whatever you want.
And so ideally, all of these would act the same. This just makes them, the curly
brackets act the same as the square and the smooth brackets.
Jon: Yeah, it’s funny, right? Because with the square brackets, this already
worked, right? If you write, like, quote![things].into()
, it would have just
worked. So this was just an oddity for— basically because curly brackets also
need to be interpreted in other contexts, and there they have a different
semantic meaning. So it’s just— it was really interesting, like, to look at the
PR too, and see the reasoning for why this didn’t work and why it now works.
Ben: Yeah, just a parser quirk, essentially.
Jon: Yeah, exactly.
Ben: All right. And that’s it.
Jon: Yeah, I think we got through all four releases. Pretty good.
Ben: And that’s an entire year’s worth of Rust development we have finished again.
Jon: Oh yeah, that’s wild.
Ben: This is our last 2021 podcast.
Jon: That’s wild. We’ve been doing this for a while now, huh?
Ben: Yeah, we should do some kind of like, celebration at some point. I guess, I don’t know.
Jon: Well, I can do that right now. (organ music)
Ben: You’re just waiting the entire time, to use the sound board, weren’t you?
Jon: Exactly. That’s great. I’m excited to see what the new year will bring. And I believe that Rust cannot get any better. So I’m assuming that all following release notes will be empty.
Ben: Oh yeah, we’re done. That’s it.
Jon: Yep. We all know that 2021 was the year of Rust on the desktop, and now it is complete. There are no more bugs. There are only feature requests.
Ben: All languages have become Rust. They all submit to Rust. There’s only Rust.
Jon: Yeah, every other language is now transpiled to Rust.
Ben: All CPUs now run Rust code directly. No need for a compiler at all.
Jon: Yeah, microcode is also written in Rust now.
Ben: It’s Rust all the way down, every circuit on your CPU, no more logic gates. No more, like, transistors, or you know, neutrons, protons et cetera. It’s all Rust code.
Jon: Yeah, it’s Rust—
Both: all the way down.
Ben: Yeah, the fabric of the entire universe.
Jon: There’s a turtle at the very bottom.
Ben: Made of Rust.
Jon: Yeah, made of Rust. Yes, it’s a rusting turtle.
Ben: All right, well, I’ll see you next year then, Jon.
Jon: I will see you next year, and enjoy the holidays.
Ben: Happy 2021, as best you can survive it, to all of our listeners.
Jon: Bye, everyone.
Ben: Bye!