Using `try_borrow`/try_borrow_mut` should avoid panics, but yes, the overhead is why it's the exception rather than the rule, and it has to be done manually with these types. I'm not making a value judgment on the utility of working with that model, only pointing out in response to the parent comment that one of the things they mention is somewhat possible today, at the cost of having to update code. Even if it were possible to do it seamless as I'm assuming they're talking about, I don't really think it would be possible to do without incurring _any_ runtime overhead, but I think that's kind of their point; it might be nice to be able to switch between models when doing different types of development.
>I recommend watching the video @nerditation linked. I believe Amanda mentioned somewhere that Polonius is 5000x slower than the existing borrow-checker; IIRC the plan isn't to use Polonius instead of NLL, but rather use NLL and kick off Polonius for certain failure cases.
Isn't this a pretty trivial observation, though? All code everywhere relies on the absence of UB. The strength of Rust comes from the astronomically better tools to avoid UB, including Miri.
Miri is good, but it still has very significant large limitations. And the recommendation of using Miri is unlikely to apply to using similar tools for many other programming languages, given the state of UB in the Rust ecosystem, as recommended by
>If you use a crate in your Rust program, Miri will also panic if that crate has some UB. This sucks because there’s no way to configure it to skip over the crate, so you either have to fork and patch the UB yourself, or raise an issue with the authors of the crates and hopefully they fix it.
>This happened to me once on another project and I waited a day for it to get fixed, then when it was finally fixed I immediately ran into another source of UB from another crate and gave up.
Further, Miri is slow to run, discouraging people to use it even for the subset of cases that it can catch UB.
>The interpreter isn’t exactly fast, from what I’ve observed it’s more than 400x slower. Regular Rust can run the tests I wrote in less than a second, but Miri takes several minutes.
If Miri runs 50x slower than normal code, it can limit what code paths people will run it with.
So, while I can imagine that Miri could be best in class, that class itself has significant limitations.
> So, while I can imagine that Miri could be best in class, that class itself has significant limitations.
Sure -- but it's still better than writing similar code in C/C++/Zig where no comparable tool exists. (Well, for C there are some commercial tools that claim similar capabilities. I have not been able to evaluate them.)
>There is no reason a language like Rust can't be designed with much more sensible strict aliasing rules.
The aliasing rules of Rust for mutable references are different and more difficult than strict aliasing in C and C++.
Strict aliasing in C and C++ are also called TBAA, since they are based on compatible types. If types are compatible, pointers can alias. This is different in Rust, where mutable references absolutely may never alias, not even if the types are the same.
Rust aliasing is more similar to C _restrict_.
The Linux kernel goes in the other direction and has strict aliasing optimization disabled.
> The aliasing rules of Rust for mutable references are different and more difficult than strict aliasing in C and C++.
"more difficult" is a subjective statement. Can you substantiate that claim?
I think there are indications that it is less difficult to write aliasing-correct Rust code than C code. Many major C codebeases entirely give up on even trying, and just set "-fno-strict-aliasing" instead. It is correct that some types are compatible, but in practice that doesn't actually help very much since very few types are compatible -- a lot of patterns people would like to write now need extra copies via memcpy to avoid strict aliasing violations, costing performance.
In contrast, Rust provides raw pointers that always let you opt-out of aliasing requirements (if you use them consistently); you will never have to add extra copies. Miri also provides evidence that getting aliasing right is not harder than dealing with other forms of UB such as data races and uninitialized memory (with Tree Borrows, those all occur about the same amount).
I would love to see someone write a strict aliasing sanitizers and run it on popular C codebases. I would expect a large fraction to be found to have UB.
I am very sorry, but your arguments here are terrible.
Unless casting or type punning through unions are used, the type system should help a lot in terms of avoiding using pointers to types that are not compatible in C. And then special care can be taken in any cases where casts are used. C++ is probably better at avoiding type casts, with all the abstractions it has.
This is different from no aliasing of Rust, where mutable references of even the same type may not alias.
Your own tool, Miri, reports that this fairly simple code snippet is UB, even though it is only the raw pointer that is dereferenced, and "a2" is not even read after assignment.
And you know better than me that Miri cannot handle everything. And Miri is slow to run, which is normal for that kind of advanced tool, not a demerit against Miri but against the general kind of tool it is.
I am very surprised that you come with arguments this poor.
You are not testing what you think you are testing.
"let &mut a2 = &mut a;" is pattern-matching away the reference, so it's equivalent to "let a2 = a;".
You're not actually casting a mutable reference to a pointer, you're casting the integer 13 to a pointer. Dereferencing that obviously produces UB.
If you fix the program ("let a2 = &mut a;"), then Miri accepts it just fine.
> Unless casting or type punning through unions are used, the type system should help a lot in terms of avoiding using pointers to types that are not compatible in C
This is simply wrong. For once, C's type system does not help you here at all. Consider the following code:
float* f = ...;
void* v = f;
long* i = v;
// Code using both *i and *f
This code has undefined behavior due to TBAA. Evidently, no unions are used in it. The type system also inserts implicit casts which can be hard to spot. This issue is not theoretical, the snippet above is taken from Quake 3's <https://en.wikipedia.org/wiki/Fast_inverse_square_root>.
Further, you just can't seriously argue that C's type system helps you avoid UB in a thread about Rust. Rust's type system is the one that helps you avoid UB, and it's just so much better at that.
The code you mentions has UB, yes, but for reasons entirely unrelated to aliasing. You're reading from the address literal 0x0000000b, which is unsurprisingly not a live allocation. It's equivalent to the following C code (which similarly has UB).
printf("%d", *(int*)(0x0000000b));
The first rule of writing safe code in Rust is "don't use unsafe." This rule is iron-clad (up to compiler bugs). You broke that rule. The second rule of writing safe code in Rust is "if you use unsafe, know what you're doing." You also broke that rule since the Rust code you wrote is probably not you wanted it to be.
But the implications of the second rule are also that you should know the aliasing model, or at least the over-approximation of "do not mix references and pointers." If you use raw pointers everywhere, you won't run into aliasing bugs.
> This is different from no aliasing of Rust, where mutable references of even the same type may not alias.
Aliasing in Rust is simpler when you follow the first rule, since everything is checked by the compiler. And if you use unsafe code with raw pointers, things are still simpler than in C since there is no TBAA. Only if you mix references and pointers do you get into territory where you need to know the aliasing model.
> This is different from no aliasing of Rust, where mutable references of even the same type may not alias.
It is different, yes. I never said it was the same. Your claim was that the Rust model is more difficult, that needs more justification than just "it is different".
Rust allows pointers of arbitrary types to alias if you use raw pointers. That kind of code is impossible to write in C. So there is a very objective sense in which the Rust variant is more expressive, i.e., it lets you do more things. That's not necessarily a measure of difficulty, but it is at least an objective way to compare the models. (But arguably, if something is impossible to do in C, that makes it more difficult than doing the same thing in Rust... ;)
Compare that to Rust, where if you hit aliasing limitations, there's always a way to change your code to still do what you need to do: use raw pointers. In other words, every UB-free C program can be translated to a UB-free Rust program that does substantially the same thing (same in-memory representation), but the other direction does not hold: translating a UB-free Rust program to UB-free C is not always possible, you might have to do extra copies.
Objectively comparing difficulty is going to be hard, so I gave some anecdotal evidence based on what we know about making real-world Rust code compatible with our aliasing models. Do you have any evidence for your claim of the C model being simpler?
(Other people already replied very well to the other points, I will not repeat them.)
And TBAA is much easier for programmers to wrangle than the aliasing of Rust, right? The corresponding aliasing feature for C would be _restrict_, which is rarely used.
Though Linus and Linux turns off even strict aliasing/TBAA.
I don't think TBAA is easier to wrangle at all, or rather, it's almost never relevant except for allowing the compiler to make obviously, trivially true assumptions. Without it (all pointers could alias all the time), any C function taking more than one pointer argument would compile to a very surprising series of instructions, reloading from memory every single time any pointer is dereferenced.
Rust's rules are very simple and easy to get right - not least because breaking them is a compiler error.
How is your comment consistent with some C compilers enabling users to disable TBAA/strict aliasing? Like the Linux kernel does. Do those codebases fit "compile to a very surprising series of instructions,"?
Yeah, concurrency bugs that only occur in very specific situations are hard to track down with a pure testing tool. However, we have some ongoing work that should make Miri a lot better at this... we are just not sure yet whether we can get it to have usable performance. ;)
> it is inconsistent with "fearless concurrency" when the Rust stdlib has UB.
It is not. "fearless X" only applies to safe code. Rust was always very clear about that. It turns out that in practice, if >90% of your code are "fearless", that actually significantly reduces the amount of bugs you have to worry about, even if the remaining 10% can still have UB. (I don't have hard numbers on how much code is unsafe, AFAIK it is much less than 10%.)
But yeah, Miri cannot find all UB bugs. We are also very clear about that. But you don't have to find all UB bugs to make progress in this space compared to the status quo in C/C++.
In C, you can alias pointers if they have compatible types. Not the case in Rust for mutable references. And the rules of Rust have tripped up even senior Rust developers.
Without MIRI, a lot of Rust developers would be lost, as they do not even attempt to understand unsafe. And MIRI cannot and does not cover everything, no matter how good and beloved it is.
It should have been possible for senior Rust developers to write UB-free code without having to hope that MIRI saves them.
The situation is not that bad. The rules of unsafe code were pretty badly defined back then, but they are in process of becoming a lot clearer, and like the grandparent argues, with a well-defined aliasing model like Tree Borrows, they are easier to understand than C's.
If you look into the code you linked, the problem was about accessing undefined bytes though an aliased, differently-typed pointer – something you would have hard time doing in C to begin with. MaybeUninit was a new thing back then. I think that nowadays, a senior Rust developer would clear the hurdles better.
I am very sorry, but you do not address that TBAA, like C has by default, generally is easier than just no aliasing, like what Rust has for mutable references. This is a major difference. C code can opt into a similar kind of aliasing, namely by using _restrict_, but that is opt-in, while it is always on for Rust.
> TBAA, like C has by default, generally is easier than just no aliasing [note: I assume no aliasing here to refer Rust's mutable references, not C's restrict]
I don't accept that statement. Rust's mutable references are statically checked, so they are impossible to get wrong (modulo compiler bugs) if you aren't using unsafe code. Using raw pointers has no no-aliasing requirements, so again, it's easier than in C.
The hard part is mixing mutable references and raw pointers. In 95% of Rust code, you are not required to do that, and you shouldn't do that. In the remaining 5%, you should understand the aliasing model. In that case, indeed, you need to know more than what TBAA requires you to know. But that's for the case where you can also _DO_ more than TBAA would allow you to do.
Yes, exactly. I'd be surprised if it's 5% -- that's about the ratio of all unsafe code, and most unsafe code does not mix references and raw pointers. I think it's less than 1% of the overall code that has to worry about this, but unfortunately I don't have hard data.
We have rate-limiters for new accounts to limit the damage that can be done by spammers, trolls and flamewar participants.
For users who have valuable contributions to make to threads and are earnest about participating positively to HN, we're happy (really, very happy) to turn off the rate limiters.
You seem to have registered multiple different accounts in order to keep participating in the thread, then hitting the rate limiters each time. Please don't do this. It's confusing for other readers and is against the guidelines [1]:
Throwaway accounts are ok for sensitive information, but please don't create accounts routinely. HN is a community—users should have an identity that others can relate to.
If you would like us to merge these accounts and turn off the rate-limiter, please email us at hn@ycombinator.com.
> And the rules of Rust have tripped up even senior Rust developers.
Yeah, even senior Rust devs make mistakes. Thanks to Miri, we can catch such mistakes. No reasonable person would expect even senior Rust devs to be magic superheroes that can write tricky unsafe code without making any mistake.
How confident are you that glibc has zero Undefined Behavior? I rather doubt it. The Rust standard library has its entire test suite (well, almost everything, except for some parts in std::fs and std::net) run through Miri. That's not a proof there's no UB in corner cases not covered by the tests, but it means we are much, much more likely to find such bugs and fix them than comparable C code.
C has an opt-out that works sometimes, if a compatible type exists. Rust has an opt-out that works always: use raw pointers (or interior mutable shared references) for all accesses, and you can stop worrying about aliasing altogether.
I think that way of describing it is really weird.
In C, you do not use any special keywords to opt into or opt out of TBAA, instead it is the rule by default that one must follow. I do not consider that 'opting out'. One can disable that in some compilers by disabling 'strict aliasing', as the Linux kernel does, but that is usually on a whole-program basis and not standard.
In Rust, using raw pointers is using a different mechanism, and mutable references are always 'no aliasing'.
An example of opting in would be C's "restrict" keyword, where one opts into a similar constraint to that of Rust's 'no aliasing' for mutable references.
>use raw pointers (or interior mutable shared references) for all accesses, and you can stop worrying about aliasing altogether.
And dereferencing a raw pointer requires 'unsafe', right? And if one messee the rules up for it, theN UB.
Can you confirm that the interaction between raw pointers and mutable references still requires care? Is this comment accurate?
>It is safe to hold a raw pointer, const T or mut T, at the same time as a mutable reference, &mut T, to the same data. However, it is Undefined Behaviour if you deference that raw pointer while the mutable reference is still live.
> Can you confirm that the interaction between raw pointers and mutable references still requires care?
Yes, that still requires care.
> In C, you do not use any special keywords to opt into or opt out of TBAA, instead it is the rule by default that one must follow
That's exactly the problem: sometimes you need to write code where there's more aliasing, and C just makes that impossible. It tells you to use memcpy instead, causing extra copies which cost performance.
In Rust, you can still write the code without the extra copies. Yes, it requires unsafe, but it's at least possible. (Remember that writing C is like having all code be unsafe, so in a comparison with C, if 10% of the Rust code needs to be unsafe that's still 90% less unsafe than C.)
That doesn't let you treat data in-place at two different types, unless one of them happens to be char. So you still need to make a copy in many cases, e.g. to convert between an array of uint32_t and an array of uint16_t.
In some cases you can use unions, but that, too, is very limited, and I am not sure it would let you do this particular case.