Pre-RFC: PlatformFrom and PlatformInto

  • Feature Name: platform_from

  • Start Date: (fill me in with today's date, YYYY-MM-DD)

  • RFC PR: (leave this empty)

  • Rust Issue: (leave this empty)

Summary

Add the following traits to core::convert and std::convert, which are identical in structure to the existing From and Into traits:

pub trait PlatformFrom<T>: Sized {
    fn platform_from(_: T) -> Self;
}

pub trait PlatformInto<T>: Sized {
    fn platform_into(self) -> T;
}

impl<T, U> PlatformFrom<T> for U where U: From<T> { ... }
impl<T, U> PlatformInto<U> for T where U: PlatformFrom<T> { ... }

In addition, U: PlatformFrom<T> for all numeric primitives T and U such that the conversion is valid on the current target platform. For example, usize: PlatformFrom<u32> on 32- and 64-bit platforms.

Motivation

The existing From and Into traits are only defined for conversions which are valid on all target platforms. This means that they are unavailable to programmers developing platform-specific code even if a given conversion is valid on the target platform. For example, code written for a 32-bit platform cannot convert from a u32 into a usize using these traits since the conversion is not valid on all platforms.

This forces programmers in this situation to resort to a combination of the as keyword and comments explaining why the conversion is guaranteed to be lossless (example). Since there is no compiler assistance, if the conversion becomes lossy in the future because the code is incorrectly ported to a different architecture, or used in a platform-agnostic context, etc, compilation will continue to succeed when it shouldn't, and the code may be wrong. In the context of unsafe code, this can lead to undefined behavior. In the context of safe code, it can still lead to unspecified behavior.

Guide-level explanation

In the core::convert and std::convert modules, the following traits are added:

pub trait PlatformFrom<T>: Sized {
    fn platform_from(_: T) -> Self;
}

pub trait PlatformInto<T>: Sized {
    fn platform_into(self) -> T;
}

impl<T, U> PlatformFrom<T> for U where U: From<T> { ... }
impl<T, U> PlatformInto<U> for T where U: PlatformFrom<T> { ... }

In addition, U: PlatformFrom<T> for all numeric primitives T and U such that the conversion is valid on the current target platform. Note that, in practice, conversions between primitives whose size is not platform-dependent are already covered by existing From impls. Thus, the only added implementations are to or from usize, isize, *const T, and *mut T.

These traits are intended to be used only in a platform-dependent context. In that context, their use is encouraged instead of the as keyword. When they are used, code which is either buggy or incorrectly ported to a different platform will stop compiling as soon as conversions which were supposed to be infallible become fallible.

Reference-level explanation

Concretely, U: PlatformFrom<T> for the following types (other valid conversions like u8 -> usize are covered by existing From implementations, and thus receive a blanket PlatformFrom implementation):

32-bit platforms

T U
u16 isize
u32 usize
i32 isize
usize u32
usize u64
usize i64
usize u128
usize i128
isize i32
isize i64
isize i128

64-bit platforms

T U
u16 isize
u32 usize
u32 isize
i32 isize
u64 usize
i64 isize
usize u64
usize u128
usize i128
isize i64
isize i128

In addition, for every T: PlatformFrom<usize> and usize: PlatformFrom<T> conversion listed in the preceding two tables, two more impls exist for *const U and *mut U, as const and mut raw pointers have the same memory layout as usize.

Drawbacks

This adds more platform-dependent APIs to core and std, which is generally something we try to avoid.

Rationale and alternatives

The two primary alternatives that the author is aware of are:

  • Continue with the status quo - this is undesirable because it leads, in practice, to programmers using as for conversions, resulting in code whose correctness is not verified by the compiler
  • Introduce platform-dependent impls of From and Into - this has been discussed, and its primary drawback is that it allows code to accidentally become platform-dependent

The design presented in this RFC avoids the problems with both of these alternatives by a) providing the authors of platform-dependent code a mechanism for conversion which provides compiler verification that conversions are lossless and, b) designing the API in a way that will discourage platform-agnostic code from accidentally using it and thus becoming dependent on platform-specific behavior.

Prior art

N/A

Unresolved questions

  • Where should these traits live? {core,std}::convert seems natural because of the existing From and Into traits, but those modules do not currently have any platform-dependent APIs.

  • What should these traits (and their methods) be named?

  • Are there any other impls that should be provided besides those listed in the Reference section?

3 Likes

Isn't usize 32 bits on a 32 bit platform?

It is. However, the only way you can do that conversion today is using as. The problem with as is that it also works for lossy conversions, so if your code depends on the conversion being lossless, it will compile in situations where the conversion is lossy even though you don't want it to. For some conversions (like u16 -> usize), you can use the From trait, but as described in the Motivation section, this trait is only implemented for conversions which are valid on all platforms that Rust supports. That doesn't help if you're trying to do something like u32 -> usize which is valid on the platform you happen to be developing for, but not on all platforms.

Oh, right! I misunderstood the sentence there.

We already have an accepted RFC resolving this problem - https://github.com/rust-lang/rfcs/blob/master/text/1868-portability-lint.md.

The From / Into should be implemented for all possible numeric types and the impls may be platform-specific, but largely non-portable impls should be linted.

Even without a general portability lint mechanism a lint for non-portable From / Into conversions can be implemented (https://github.com/rust-lang/rust/pull/37423).

4 Likes

I know compiler feature-gating is usually only for unstable features, but would for this to retain feature gating after stablization. It would be nice if it still required a feature to enable, so you could have some files which enable the these Platform traits, and others which you don't have to worry about usage of these traits appearing in even though the target platform does support them.

Ah, I didn't realize that the portability lint was intended to cover this case as well.

Would it be possible to resurrect a PR like this one? I didn't quite follow the discussion about "scenarios" that led to it being closed, but it looks like scenarios were the precursor idea to the portability lint?

Probably yes.
Given that the portability lint RFC lies unimplement for years, a smaller focused solution could be acceptable.

OK, cool. Do you think the same approach you used in that PR would still work? If so, I'd be happy to re-submit it (or we could just re-open that same PR I suppose).

That PR is 3.5 years old, it won't work as is and probably needs to be largely reimplemented.

OK, sounds good!

Edit: I've submitted an issue here and submitted it to TWiR's CFP.

I wrote https://crates.io/crates/usize_cast a few days ago (it focus only on from/into usize/isize).

I agree these conversions should be in core.

So far the precedent in the standard library is that non-portable APIs should require importing something from the std::os module, in a sub-module named after the relevant platform. For example std::os::windows::process::CommandExt. This shows to readers of the code:

  1. That something non-portable is used
  2. On what platforms that code is expected to compile

This RFC achieves 1. through having Platform in the traits name, but fails at 2: when reading foo.platform_into() I don’t know what platforms this is expected to be compatible with. I can figure it out based on the input and return type of the conversion, but those are often not obvious because of type inference.

I agree that there is a need here, but I’d prefer a solution where which platforms to target is an explicit choice. This could be some new modules like std::os::ptr_size_ge_32bits or std::os::ptr_size_le_64bits where ge and lemean "greater or equal" and "less than or equal", like in corresponding methods of the PartialOrd trait. (Names to be bikesheded.)

I have personally run into portability issues from a crate defining and using such a conversion in their public API without it being obvious