Expressions

Guideline: Avoid as underscore pointer casts gui_HDnAZ7EZ4z6G
status: draft
tags: readability, reduce-human-error
category: required
decidability: decidable
scope: module
release: <TODO>

Code must not rely on Rust’s type inference when doing explicit pointer casts via var as Type or core::mem::transmute. Instead, explicitly specify the complete target type in the as expression or core::mem::transmute call expression.

Rationale: rat_h8LdJQ1MNKu9
status: draft
parent needs: gui_HDnAZ7EZ4z6G

var as Type casts and core::mem::transmutes between raw pointer types are generally valid and unchecked by the compiler as long the target pointer type is a thin pointer. Not specifying the concrete target pointer type allows the compiler to infer it from the surroundings context which may result in the cast accidentally changing due to surrounding type changes resulting in semantically invalid pointer casts.

Raw pointers have a variety of invariants to manually keep track of. Specifying the concrete types in these scenarios allows the compiler to catch some of these potential issues for the user.

Non-Compliant Example: non_compl_ex_V37Pl103aUW4
status: draft
parent needs: gui_HDnAZ7EZ4z6G

The following code leaves it up to type inference to figure out the concrete types of the raw pointer casts, allowing changes to with_base’s function signature to affect the types the function body of non_compliant_example without incurring a compiler error.

#[repr(C)]
struct Base {
   position: (u32, u32)
}

#[repr(C)]
struct Extended {
   base: Base,
   scale: f32
}

fn non_compliant_example(extended: &Extended) {
   let extended = extended as *const _;
   with_base(unsafe { &*(extended as *const _) })
}

fn with_base(_: &Base) { ... }
Compliant Example: compl_ex_W08ckDrkOhkt
status: draft
parent needs: gui_HDnAZ7EZ4z6G

We specify the concrete target types for our pointer casts resulting in a compilation error if the function signature of with_base is changed.

#[repr(C)]
struct Base {
   position: (u32, u32)
}

#[repr(C)]
struct Extended {
   base: Base,
   scale: f32
}

fn non_compliant_example(extended: &Extended) {
   let extended = extended as *const Extended;
   with_base(unsafe { &*(extended as *const Base) })
}

fn with_base(_: &Base) { ... }
Guideline: The ``as`` operator should not be used with numeric operands gui_ADHABsmK9FXz
status: draft
tags: subset, reduce-human-error
category: advisory
decidability: decidable
scope: module
release: <TODO>

The binary operator as should not be used with:

  • a numeric type, including all supported integer, floating, and machine-dependent arithmetic types; or

  • bool; or

  • char

as either the right operand or the type of the left operand.

Exception: as may be used with usize as the right operand and an expression of raw pointer type as the left operand.

Rationale: rat_v56bjjcveLxQ
status: draft
parent needs: gui_ADHABsmK9FXz

Although the conversions performed by as between numeric types are all well-defined, as coerces the value to fit in the destination type, which may result in unexpected data loss if the value needs to be truncated, rounded, or produce a nearest possible non-equal value.

Although some conversions are lossless, others are not symmetrical. Instead of relying on either a defined lossy behaviour or risking loss of precision, the code can communicate intent by using Into or From and TryInto or TryFrom to signal which conversions are intended to perfectly preserve the original value, and which are intended to be fallible.

A pointer-to-address cast does not lose value, but will be truncated unless the destination type is large enough to hold the address value. The usize type is guaranteed to be wide enough for this purpose.

A pointer-to-address cast is not symmetrical because the resulting pointer may not point to a valid object, may not point to an object of the right type, or may not be properly aligned. If a conversion in this direction is needed, std::mem::transmute will communicate the intent to perform an unsafe operation.

Non-Compliant Example: non_compl_ex_hzGUYoMnK59w
status: draft
parent needs: gui_ADHABsmK9FXz

as used here can change the value range or lose precision. Even when it doesn’t, nothing enforces the correct behaviour or communicates whether we intend to allow lossy conversions, or only expect valid conversions.

fn f1 (x:u16, y:i32, z:u64, w:u8) {
  let a = w as char;           // non-compliant
  let b = y as u32;            // non-compliant - changes value range
  let c = x as i64;            // non-compliant - could use .into()

  let d = y as f32;            // non-compliant - lossy
  let e = d as f64;            // non-compliant - could use .into()
  let f = e as f32;            // non-compliant - lossy

  let g = e as i64;            // non-compliant - lossy despite object size

  let p1:* const u32 = &b;
  let a1 = p1 as usize;        // compliant by exception
  let a2 = p1 as u16;          // non-compliant - may lose address range
  let a3 = p1 as u64;          // non-compliant - use usize to indicate intent

  let p2 = a1 as * const u32;  // non-compliant - prefer transmute
  let p3 = a2 as * const u32;  // non-compliant, and probably invalid
}
Compliant Example: compl_ex_uilHTIOgxD37
status: draft
parent needs: gui_ADHABsmK9FXz

Valid conversions that are guaranteed to preserve exact values can be communicated better with into() or from(). Valid conversions that risk losing value, where doing so would be an error, can communicate this and include an error check, with try_into or try_from. Other forms of conversion may find transmute better communicates their intent.

fn f2 (x:u16, y:i32, z:u64, w:u8) {
  let a:char            = w.into ();
  let b:Result <u32, _> = y.try_into (); // produce an error on range clip
  let c:i64             = x.into ();

  let d = f32::from (x);  // u16 is within range, u32 is not
  let e = f64::from (d);
  // let f = f32::from (e); // no From exists

  // let g = ...            // no From exists

  let h:u32 = 0;
  let p1:* const u32 = &h;
  let a1 = p1 as usize;     // (compliant)

  unsafe {
    let a2:usize = std::mem::transmute (p1);  // OK
    let a3:u64   = std::mem::transmute (p1);  // OK, size is checked
    // let a3:u16   = std::mem::transmute (p1);  // invalid, different sizes

    let p2:* const u32 = std::mem::transmute (a1); // OK
    let p3:* const u32 = std::mem::transmute (a1); // OK
  }

  unsafe {
    let f1:f64 = std::mem::transmute (z); // does something entirely different
  }
}
Guideline: An integer shall not be converted to a pointer gui_PM8Vpf7lZ51U
status: draft
tags: subset, undefined-behavior
category: required
decidability: decidable
scope: module
release: <TODO>

The as operator shall not be used with an expression of numeric type as the left operand, and any pointer type as the right operand.

std::mem::transmute shall not be used with any numeric type (including floating tyoes) as the argument to the Src parameter, and any pointer type as the argument to the Dst parameter.

Rationale: rat_YqhEiWTj9z6L
status: draft
parent needs: gui_PM8Vpf7lZ51U

A pointer created from an arbitrary arithmetic expression may designate an invalid address, including an address that does not point to a valid object, an address that points to an object of the wrong type, or an address that is not properly aligned. Use of such a pointer to access memory will result in undefined behavior.

The as operator also does not check that the size of the source operand is the same as the size of a pointer, which may lead to unexpected results if the address computation was originally performed in a differently-sized address space.

Non-Compliant Example: non_compl_ex_0ydPk7VENSrA
status: draft
parent needs: gui_PM8Vpf7lZ51U

Any use of as or transmute to create a pointer from an arithmetic address value is non-compliant:

fn f1 (x:u16, y:i32, z:u64, w:usize) {
  let p1 = x as * const u32;  // not compliant
  let p2 = y as * const u32;  // not compliant
  let p3 = z as * const u32;  // not compliant
  let p4 = w as * const u32;  // not compliant despite being the right size

  let f:f64 = 10.0;
  // let p5 = f as * const u32;  // not valid

  unsafe {
    // let p5:* const u32 = std::mem::transmute (x);  // not valid
    // let p6:* const u32 = std::mem::transmute (y);  // not valid

    let p7:* const u32 = std::mem::transmute (z); // not compliant
    let p8:* const u32 = std::mem::transmute (w); // not compliant

    let p9:* const u32 = std::mem::transmute (f); // not compliant, and very strange
  }
}
Compliant Example: compl_ex_oneKuF52yzrx
status: draft
parent needs: gui_PM8Vpf7lZ51U

There is no compliant example of this operation.

Guideline: An integer shall not be converted to an invalid pointer gui_iv9yCMHRgpE0
status: draft
tags: defect, undefined-behavior
category: required
decidability: undecidable
scope: system
release: <TODO>

An expression of numeric type shall not be converted to a pointer if the resulting pointer is incorrectly aligned, does not point to an entity of the referenced type, or is an invalid representation.

Rationale: rat_OhxKm751axKw
status: draft
parent needs: gui_iv9yCMHRgpE0

The mapping between pointers and integers must be consistent with the addressing structure of the execution environment. Issues may arise, for example, on architectures that have a segmented memory model.

Non-Compliant Example: non_compl_ex_CkytKjRQezfQ
status: draft
parent needs: gui_iv9yCMHRgpE0

This example makes assumptions about the layout of the address space that do not hold on all platforms. The manipulated address may have discarded part of the original address space, and the flag may silently interfere with the address value. On platforms where pointers are 64-bits this may have particularly unexpected results.

fn f1 (flag:u32, ptr:* const u32) {
  /* ... */
  let mut rep = ptr as usize;
  rep = (rep & 0x7fffff) | ((flag as usize) << 23);
  let p2 = rep as * const u32;
}
Compliant Example: compl_ex_oBoluiKSvREu
status: draft
parent needs: gui_iv9yCMHRgpE0

This compliant solution uses a struct to provide storage for both the pointer and the flag value. This solution is portable to machines of different word sizes, both smaller and larger than 32 bits, working even when pointers cannot be represented in any integer type.

struct PtrFlag {
  pointer:* const u32,
  flag:u32
}

fn f2 (flag:u32, ptr:* const u32) {
  let ptrflag = PtrFlag {
    pointer: ptr,
    flag: flag
  };
  /* ... */
}