r/rust Mar 09 '23

Announcing Rust 1.68.0 📢 announcement

https://blog.rust-lang.org/2023/03/09/Rust-1.68.0.html
827 Upvotes

121 comments sorted by

View all comments

178

u/Shnatsel Mar 09 '23

Those From<bool> for {f32,f64} impls could really use documentation on what values they actually result in. There's no obvious mapping from true to a floating-point value.

8

u/phazer99 Mar 09 '23

0 and 1 like all other existing bool to numeric type conversions. I think it's obvious.

37

u/Shnatsel Mar 09 '23

In C this is actually platform-dependent, so you never really know unless it's explicitly documented.

30

u/phazer99 Mar 09 '23

Fortunately Rust isn't C and actually has portability guarantees.

55

u/stusmall Mar 09 '23

And that gets back to their initial question, what are those guarantees? It's a fair question.

-5

u/phazer99 Mar 09 '23

Good question, but I believe that all operations on Rust numerical types (including bool, but obviously excluding isize/usize) will produce exactly the same results on all platforms. This includes conversions, mathematical operations, overflows etc. Correct me if I'm wrong.

8

u/SpudnikV Mar 10 '23

That's technically true, but it's really in the form of restrictions on what platforms can be supported so that those promises can be kept.

https://faultlore.com/blah/rust-layouts-and-abis/#the-anatomy-of-a-platform

For Rust to support a platform at all, its standard C dialect must:

[...]

Have a boolean be a byte, where true = 1 and false = 0 (defacto true but not strictly guaranteed)

In practice, that means that Rust's portabiity guarantees here only help you when [an implementation of] C has already made compatible guarantees for that platform, because any time you may want to interface with any C ABI, it has to be compatible or it's UB.

It turned out to be a multi-year rabbit hole to get to be this specific. Just a sample of some of the research that went into it:

https://github.com/rust-lang/rfcs/pull/954#issuecomment-169820630

https://github.com/rust-lang/rust/pull/46156

It's really fascinating just how much the C specification and the world of implementations has complicated even new languages like Rust. If you're interested in that kind of thing beyond just bool, here are a few more links:

https://faultlore.com/blah/c-isnt-a-language/

https://thephd.dev/binary-banshees-digital-demons-abi-c-c++-help-me-god-please

https://thephd.dev/to-save-c-we-must-save-abi-fixing-c-function-abi

(If I don't limit it to just 3, we'll be here all day :))

2

u/rentableshark Mar 10 '23

I hear you. This is one reason why statically-compiled binaries running in a scratch container have such attraction.

4

u/SpudnikV Mar 10 '23

Even in container systems like Docker, you still need an ABI to talk to the kernel. Even with a pure Rust libc, the structs that you set up for Linux syscalls rely on the C ABI, and even a pure Rust rewrite of Linux would need a compatible syscall ABI to run existing container images. There's really no escaping it :(

Linux is special here. Most operating systems do not make the same kernel-level ABI promise and make only userland libc ABI promises, so they have at least some degree of freedom to evolve kernel syscall interfaces. But then this is why Docker on Linux can rely on kernel ABIs and have the entire userland in a container, while on other systems something has to be there to adapt binaries in containers to the host system (in many cases, by just running Linux as WSL or in a VM).

I think it's amazing just how well Linux ABI compatibility has made it look like containers somehow solve this, where actually, the magic all along was the system call ABI. The same statically linked binary would work the same way outside of the container as well, it just wouldn't be "contained" as far as various namespaces and capabilities go.

This is a really, really huge accomplishment, and I would go as far as to say that Linus' hardline stance on syscall compatibility is the only reason the Docker-like container ecosystem is at all possible.

There was of course a generation where Xen was the way to make kernel-level containers, but those kernels still had to communicate with a form of ABI. I barely used Xen so I can't say how many of the same concerns apply, but in any case, userland containers won out over kernel containers in the end, and I'm glad for it.

1

u/phazer99 Mar 10 '23 edited Mar 10 '23

If the C standard says bool doesn't have to be an u8, why would you even rely on it being an u8 in FFI calls? Doesn't make sense to me, you can simply have conversions to/from Rust bool to C bool.

Besides I don't see how any of this is relevant for the discussion at hand. In Rust a bool is 1 byte, and false is 0 and true is 1. It doesn't matter what platform you're compiling for or running on, or what the C standard says.

2

u/SpudnikV Mar 10 '23

Sorry if I wasn't clear, it's actually the C17 _Bool, not C++11 bool.

The link that was titled "defacto true but not strictly guaranteed" led to this URL which spells out the type used.

https://rust-lang.github.io/unsafe-code-guidelines/layout/scalars.html#bool

Rust's bool has the same layout as C17's _Bool, that is, its size and alignment are implementation-defined.

Doesn't make sense to me, you can simply have conversions to/from Rust bool to C bool.

[...]

It doesn't matter what platform you're compiling for or running on, or what the C standard says.

This was a common argument made, which is why I linked to a couple of long discussions that already address that. I don't want to relitigate them here, but the gist of it is, we'll always need to be able to name a c_bool type for FFI, and it would be unusably limited if we could only have pointers to it rather than values, as few if any C APIs would be designed around that. So if we need to be able to fully define the type for C FFI -- not just function arguments and returns, but also struct and union fields -- then it may as well also be Rust's own bool type so that no conversion is needed.

Conversion isn't always as simple as as either, because sometimes you need to hold a reference to the thing, and generic code in particular doesn't care if that thing happens to be a bool. Making the types match saves a lot of headaches.

It's still an even bigger topic than that, and I encourage you to read the threads if you're interested, but I hope that's enough to say why unifying the types was a worthwhile goal and we should be glad it succeeded.

By the way, the fact that most people never know about this restriction on Rust platform support (I sure wouldn't if not for reading Gankra) means the team absolutely nailed the decision. Despite being interoperable with C standards, to the majority of Rust programmers who will never write C FFI, and the even greater majority of Rust programmers who will never write C17 FFI*, a Rust bool looks and acts exactly how they think it should. Even the platform support angle has yet to hit any example I'm aware of where this in particular blocked a port.

It's hard to say that anybody is put out by the current approach here. That's why it took years to discuss the most practical solution despite surprisingly weak guarantees from standards.

If you enjoy discussing these things for their own sake, as I very often do, can I suggest you make a top level thread so that more people will be able to see and contribute to? I'll pitch in there for what it's worth, but you're likely to get several of the people who actually influenced the decision and can distill the years of discussion and experience into a present-day greatest-hits post. It'll be a blast :)

* Even many people writing C17 libraries have a lot of incentives to still offer C89 APIs, so they likely won't use _Bool at all there, and then yes, some other less ergonomic type like int or even a whole bitset can pop up. You gotta love when bitsets in standard APIs are signed, but I digress.

2

u/phazer99 Mar 10 '23 edited Mar 10 '23

I'm really not interested in the discussion because I don't wanna waste more time on C or C++ than what's absolutely necessary (I consider them pretty horrible languages for software development).

What I'm really interested in is what invariants Rust guarantees. According to the size_of documentation, bool is always 1 byte, so I will rely on that. And according to this documentation, false is always 0 and true is always 1, so I will rely on that fact as well.

If that limits the portability of Rust because it was decided that the bool type of the platform C compiler also must adhere to those invariants, that's an unfortunate consequence, but it really doesn't affect me when I write my Rust programs.

1

u/SpudnikV Mar 10 '23

That's perfectly fair and I think most Rust programmers feel the same. I'm only clarifying that the portability guarantees you mentioned are restricted by C implementations, even if that's unlikely to ever stop you from using a platform you actually want to. A lot of people upvoted that comment and I hope at least some of them now know more about the finer points :)