|
|
Subscribe / Log in / New account

Are casts encouraged in Rust?

Are casts encouraged in Rust?

Posted Jun 29, 2025 20:28 UTC (Sun) by NYKevin (subscriber, #129325)
In reply to: Are casts encouraged in Rust? by alx.manpages
Parent article: How to write Rust in the kernel: part 2

Rust has no implicit integer promotions, so all integer conversions must be written explicitly, usually in one of three forms:

* uapi::BMCR_SPEED100.into() - If the value is of a type that can be losslessly converted into the desired type, this is done. Otherwise, it is a compile error. The compiler attempts to infer the type we're converting into from context, but it can be spelled out explicitly (and this must be done if it is ambiguous). This can be equivalently written as u16::from(uapi::BMCR_SPEED100) (which is always unambiguous if we assume the argument's type is known).
* uapi::BMCR_SPEED100.try_into() - Returns Result<u16>, an enum that is either Ok(some value) or Err(some error). You can then handle the possible error value in various ways. For numeric conversions, you get an error if it would overflow or lose precision. Again, you can equivalently write u16::try_from(uapi::BMCR_SPEED100).
* uapi::BMCR_SPEED100 as T - A type cast. For numeric types, overflow is handled by truncation (for more details, see [1]). You may only do this for a relatively short list of specific fundamental types, and generally only in cases where it's "obvious" how the compiler should handle it. More importantly, all of these operations are safe and cannot produce UB by themselves (but you can cast between unrelated raw pointer types, and the compiler will just let it happen because raw pointers have no safety properties until you try to dereference them).

Generally, "as" casting is dispreferred compared to the other two options. Despite being safe (in the sense of no UB), it can lose data as you point out.

Unfortunately, neither into() nor try_into() are allowed in const contexts, because those are general conversion traits for converting between arbitrary types (i.e. there's no guarantee they can be evaluated at compile time for all types, so you're not allowed to use them for any types). This is a limitation that the Rust folks are interested in lifting, but it has (apparently) been through multiple rounds of bikeshedding and is still under discussion (see [2]), so I imagine that the Rust-for-Linux folks will not have enabled the relevant unstable feature gate (I did not actually check).

Technical nitpick: It is possible to define into() or try_into() without defining the corresponding from(), and then they are not actually equivalent. This is discouraged in the Into<T> and From<T> docs, which clearly spell out that you should define from() and then the into() will be defined automatically.

[1]: https://doc.rust-lang.org/reference/expressions/operator-...
[2]: https://github.com/rust-lang/rust/issues/67792


to post comments

Are casts encouraged in Rust?

Posted Jun 30, 2025 2:13 UTC (Mon) by alx.manpages (subscriber, #145117) [Link] (9 responses)

Thanks! If they're trying to fix it I guess that's good news.

> Despite being safe (in the sense of no UB), it can lose data as you point out.

Which amounts to being usafe (regardless of it being memory-safe). Logic bugs can be security bugs.

Are casts encouraged in Rust?

Posted Jun 30, 2025 2:45 UTC (Mon) by NYKevin (subscriber, #129325) [Link]

Rust uses the word "safe" in a highly specific way. If an operation cannot cause UB, either alone or in conjunction with other safe operations, then it is considered safe. The word "safe" does not mean correct, valid, reasonable, or a good idea.

The reason for this framing is so that unsafe operations can be protected by the unsafe keyword. Determining whether an operation is "correct" is clearly beyond the capabilities of a compiler to prove in full generality, so "no UB" is considered an acceptable substitute. When correctness is desired, the usual approach is to make incorrect states impossible to represent, by constructing a type in such a way that all of its valid instances represent valid states or operations. Since producing an invalid instance of a type (e.g. an enum instance which is not any of the enum's variants, a bool with a value other than true or false, or any uninitialized value that occupies nonzero space and isn't MaybeUninit<T>) is considered UB in Rust, this has the practical effect of tying correctness to safety for the purpose of that specific type. If you find a way to do something like this over an entire program, then in principle you can use the Curry-Howard isomorphism to convert that type construction into a proof of correctness, which in turn could be used to formally verify the program. But that kind of construction can get very complicated, and may not be worth it in all situations, hence the existence of unsafe as an escape hatch.

Are casts encouraged in Rust?

Posted Jun 30, 2025 7:38 UTC (Mon) by tialaramex (subscriber, #21167) [Link]

Lots of useful ideas, perhaps beyond what a novice Rust programmer would realise, are trapped behind the problem that trait implementations in Rust aren't today able to be marked constant.

An example beyond what you might expect is that for-each loops can't be constant. Rust's for-each loops always de-sugar into use of the trait call IntoIterator::into_iter to make whatever you've got into an iterator to begin with. This happens even if what you've provided is already an iterator, such conversion is just the identity function so your optimiser will elide the work - so until that trait implementation can be constant itself, the entire for-each loop feature isn't available in constants. You can write a while loop, for example, and that works fine, but not a for-each loop.

Are casts encouraged in Rust?

Posted Jun 30, 2025 19:29 UTC (Mon) by iabervon (subscriber, #722) [Link] (6 responses)

I think there's a different set of considerations for compile-time constants than for runtime casts; having "const x: u16 = 65536 as u16;" (or some other syntax for it) shouldn't result in an executable that unconditionally halts immediately with an error message, it should result in a compile error because that's not a sensible way to make a constant 0, but it also can't do anything else.

Are casts encouraged in Rust?

Posted Jun 30, 2025 19:53 UTC (Mon) by alx.manpages (subscriber, #145117) [Link] (5 responses)

In C both compile-time (constexpr) and run-time get a diagnostic, but they're different categories of diagnostics, so you can decide which to turn on and/or off.
alx@debian:~/tmp$ cat c.c | grep -nT ^
                  1:	#include <stdint.h>
                  2:	#include <stdlib.h>
                  3:
                  4:	constexpr uint16_t  X = 65536;
                  5:	constexpr uint16_t  Y = (uint16_t) 65536;
                  6:
                  7:	int
                  8:	main(void)
                  9:	{
                 10:		uint32_t  zz = rand(); // generate a run-time u32 for line 14
                 11:
                 12:		const uint16_t  x = 65536;
                 13:		const uint16_t  y = (uint16_t) 65536;
                 14:		const uint16_t  z = zz;
                 15:	}
alx@debian:~/tmp$ clang -Weverything -Wno-unused -Wno-pre-c23-compat -Wno-c++98-compat -std=c23 c.c 
c.c:4:25: error: constexpr initializer evaluates to 65536 which is not exactly representable in type 'const uint16_t' (aka 'const unsigned short')
    4 | constexpr uint16_t  X = 65536;
      |                         ^
c.c:4:25: warning: implicit conversion from 'int' to 'uint16_t' (aka 'unsigned short') changes value from 65536 to 0 [-Wconstant-conversion]
    4 | constexpr uint16_t  X = 65536;
      |                     ~   ^~~~~
c.c:10:17: warning: implicit conversion changes signedness: 'int' to 'uint32_t' (aka 'unsigned int') [-Wsign-conversion]
   10 |         uint32_t  zz = rand(); // generate a run-time u32 for line 14
      |                   ~~   ^~~~~~
c.c:14:22: warning: implicit conversion loses integer precision: 'uint32_t' (aka 'unsigned int') to 'uint16_t' (aka 'unsigned short') [-Wimplicit-int-conversion]
   14 |         const uint16_t  z = zz;
      |                         ~   ^~
c.c:12:22: warning: implicit conversion from 'int' to 'uint16_t' (aka 'unsigned short') changes value from 65536 to 0 [-Wconstant-conversion]
   12 |         const uint16_t  x = 65536;
      |                         ~   ^~~~~
4 warnings and 1 error generated.
I think this is more sensible than the Rust approach.

Are casts encouraged in Rust?

Posted Jun 30, 2025 20:51 UTC (Mon) by NYKevin (subscriber, #129325) [Link] (2 responses)

> I think this is more sensible than the Rust approach.

I disagree.

1. The first error ("constexpr initializer evaluates to 65536...") is also a hard error in Rust by default (see https://doc.rust-lang.org/rustc/lints/listing/deny-by-def...). You can turn it into a warning if you really want to, but I've never heard of anyone choosing to do that.
2. Any warning that mentions an "implicit conversion" in C is a hard error in Rust, and can't be turned into a warning, because Rust simply does not implement those implicit conversions in the first place. You have to write explicit casts or into()/try_into().

Since I can't imagine you are disagreeing with Rust's handling of (1), that leaves us with (2). But I have to say, I have never encountered a situation where C's implicit conversions were anything other than a headache to deal with. I do not want the language magicking my data into a different type without telling me, especially in contexts where I never even asked for a conversion, such as the following:

uint16_t x = 1; // 1 converted from int to uint16_t
uint16_t y = x + 1; // Both operands converted to int, added, then converted back to uint16_t.
// Sure, *this* case is trivial and safe, but is that true every time you write something like this?

In Rust, the literal 1 is ambiguous, but would be interpreted as 1u16 in context (i.e. "a u16 with the value 1"), which is exactly what a reasonable person would expect it to mean.

Are casts encouraged in Rust?

Posted Jun 30, 2025 22:56 UTC (Mon) by alx.manpages (subscriber, #145117) [Link] (1 responses)

This is an interesting discussion. You have a point, which I also made with someone else recently.

Your right in calling C's implicit conversions messy, but it's not true of all of them.

C has three types of implicit conversions: - Integer promotions. These trigger for any fundamental type narrower than an int. I've called them a "cancer" myself recently. They are there because of historical reasons. I would remove them from the language if I could, but of course we can't at this time.

It is bad that a uint16_t is promoted to an int on almost every situation, which even changes its signedness.

The good thing is that few people actually use narrow integers like short, int16_t, or uint8_t.

The better thing is that the new _BitInt(N) integers added in C23 don't have integer promotions: a _BitInt(16) will not be promoted to an int.

So, I'd say we've partially solved this issue in C. Although we're not over. We also need to be able to specify literals of such types. I've written a proposal for the C Committee (and an extension request to both GCC and Clang) for that:

<https://github.com/llvm/llvm-project/issues/129256>

- Usual arithmetic conversions.

When adding, comparing or otherwise using two different types of integers in an operator that takes two operands, these trigger.

So, if you have
int   a = 42;
long  b = 7;

if (a < b)
    return a;
you'll get the usual arithmetic conversions to turn that int into a long. Since both retain the original signedness, this conversion is harmless, and doesn't trigger any diagnostics at all. This is a good conversion.

If you had that comparison between integers of different signedness, you could get a warning with -Wsign-compare or -Wsign-conversion (depending if you're comparing them or adding/multiplying/... them, but they're both essentially the same thing).
alx@debian:~/tmp$ cat c.c 
int
main(void)
{
	int            a = 42;
	unsigned long  b = 7;

	if (a < b)
		return 0;
}
alx@debian:~/tmp$ clang -Weverything c.c 
c.c:7:8: warning: comparison of integers of different signs: 'int' and 'unsigned long' [-Wsign-compare]
    7 |         if (a < b)
      |             ~ ^ ~
1 warning generated.
and
alx@debian:~/tmp$ cat c.c
int
main(void)
{
	unsigned int  a = 42;
	int           b = 7;

	if (a < b)
		return 0;
}
alx@debian:~/tmp$ clang -Weverything c.c 
c.c:7:8: warning: comparison of integers of different signs: 'unsigned int' and 'int' [-Wsign-compare]
    7 |         if (a < b)
      |             ~ ^ ~
1 warning generated.
This is sadly not turned on on -Wall -Wextra, but this is one diagnostic that you'd usually want, and most of the times it uncovers subtle bugs.

I said "could", because that diagnostic is not always triggered. It triggers if there can be information loss. There's a case where there can't be information loss: the unsigned integer is turned into a wider signed integer type that can represent all of the values that the unsigned integer can hold:
alx@debian:~/tmp$ cat c.c 
int
main(void)
{
	unsigned int  a = 42;
	long          b = 7;

	if (a < b)
		return 0;
}
alx@debian:~/tmp$ clang -Weverything c.c 
alx@debian:~/tmp$ 


This is another good conversion you want to happen. It's good that we don't diagnose it. - And then there are implicit conversions as if by assignment.

The C standard describes all implicit conversions as if by simple assignment. These happen, for example, when you assign some integer to a variable of another integer type.

This can be a narrowing conversion, in which case you'll get a very explicit diagnostic:
alx@debian:~/tmp$ cat c.c 
int
main(void)
{
	long l = 42;
	int i = l;
}
alx@debian:~/tmp$ clang -Weverything -Wno-unused c.c 
c.c:5:10: warning: implicit conversion loses integer precision: 'long' to 'int' [-Wshorten-64-to-32]
    5 |         int i = l;
      |             ~   ^
1 warning generated.
Again, this is not in -Wall -Wextra, but you probably want to turn on -Wshorten-64-to-32 (and similar ones) for your projects, and disable it only when you know those conversions are good.

I personally disable it selectively in a few places with
#pragma clang diagnostic ignored "-Wshorten-64-to-32"
in a few places in a library where I know that's exactly what I want.

It can also be a sign-changing conversion:
alx@debian:~/tmp$ cat c.c 
int
main(void)
{
	unsigned int l = 42;
	int i = l;
}
alx@debian:~/tmp$ clang -Weverything -Wno-unused c.c 
c.c:5:10: warning: implicit conversion changes signedness: 'unsigned int' to 'int' [-Wsign-conversion]
    5 |         int i = l;
      |             ~   ^
1 warning generated.


which is covered by the same -Wsign-conversion I mentioned earlier, which you also want on all the time, with a few exceptions maybe.

---

So, the -Wall -Wextra compiler diagnostics are a bit lacking, but if you turn on all available diagnostics, they're quite safe. Rust's .into() seems like C's behavior when the diagnostics are on, except that it doesn't allow the few conversions that don't produce any diagnostic in C, and which are actually Good Conversions. Also, Rust's .into() is just typographic noise, because good conversions is what I want all the time.

---

Then there's the issue that Rust is unable to do .into() with constant expressions, which forces you to use casts. That's worse than not allowing the good conversions without noise; this is plain dangerous.

Are casts encouraged in Rust?

Posted Jul 1, 2025 1:03 UTC (Tue) by NYKevin (subscriber, #129325) [Link]

> Rust's .into() seems like C's behavior when the diagnostics are on, except that it doesn't allow the few conversions that don't produce any diagnostic in C, and which are actually Good Conversions. Also, Rust's .into() is just typographic noise, because good conversions is what I want all the time.

This is a matter of opinion. I want to do all my conversions at the system boundaries, and then use the proper types throughout the program (preferably full-blown structs and enums, not just raw i32 or whatnot), with minimal or no further conversions after data has been ingested.

Anyway, I think I figured out why Rust does not allow that. Unlike all the other binary operators, the shift operators do support arbitrary mixing of integer types. However, their documentation pages point out that Rust applies a special rule when doing type inference: In the expression a << b (or a >> b), if Rust knows that a and b are both integers (of some possibly-unknown type), then the type inference system is special-cased to infer that the shift expression has the same type as a.

That actually tells me a lot. First of all, if you don't have that rule, it must cause issues, probably because whenever b is ambiguous, type inference can't figure out which overload to use, and just gives up rather than trying each in turn. Trying each in turn would produce the same result in this case (type of the output is the same as type of the left operand), but Rust is not always willing to do that if it can't prove that there's a unique solution (Rust is not C++ and does not want to reinvent SFINAE etc.). But that in turn means that the special case has to be really simplistic, and in particular, it must be possible to apply with partial information - if you only know the type of a and not the type of b, the rule allows you to make progress, because it only depends on the type of a. If you only know the type of b, then the rule doesn't help at all, but at least it does something in the other case. Finally, if you know neither type, then you can at least try to unify the output type with the left operand's type, and maybe that will tell you something.

So, if we wanted to allow a + b with mixed types, and make the output type be the *wider* of the two (instead of the left operand's type), then we'd probably have to give up on this idea of special-casing the type inference machinery (there's no rule you can come up with that will allow making forward progress when one and only one of the types is known). That in turn would lead to whatever problems they were originally having with a << b (i.e. probably the compiler asks for way too many type annotations).

Are casts encouraged in Rust?

Posted Jul 1, 2025 1:00 UTC (Tue) by iabervon (subscriber, #722) [Link] (1 responses)

I'd actually say that the C compilers are suboptimal here because they don't give a warning about line 13: if you want to get 0 directly, you should just use 0, and, in the more likely event that 65336 is coming from a macro or other constant and you want to just get the low bits into this constant (with the high bits going elsewhere), you should probably write it as VALUE & 0xffff, and I don't see any reason that writing (uint16_t) VALUE shouldn't give you a warning about the explicit cast resulting in a different constant value.

Are casts encouraged in Rust?

Posted Jul 1, 2025 2:14 UTC (Tue) by alx.manpages (subscriber, #145117) [Link]

> I'd actually say that the C compilers are suboptimal here because they don't give a warning about line 13

Line 13 has a cast, which precisely means: "compiler, please shut up".

If you want a diagnostic, remove the cast. As I've said, the appropriate number of casts in almost any given program is 0.


Copyright © 2025, Eklektix, Inc.
Comments and public postings are copyrighted by their creators.
Linux is a registered trademark of Linus Torvalds