10

Rust's SemVer Snares: Sizedness and Size

 3 years ago
source link: https://jack.wrenn.fyi/blog/semver-snares-size/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

(Part of an ongoing series!)

In Rust, changes to a type's size are not usually understood to be Breaking Changes™. Of course, that isn't to say you can't break safe downstream code by changing the size of a type...

Sizedness

For one, you can change the sizedness of a type, by adding an unsized field:

pub mod upstream {
  #[repr(C)]
  pub struct Foo {
    bar: u8,
    // uncommenting this field is a breaking change:
    /* baz: [u8] */
  }
}

pub mod downstream {
  use super::upstream::*;

  fn example(foo: Foo) {
    todo!()
  }
}
error[E0277]: the size for values of type `[u8]` cannot be known at compilation time
  --> src/lib.rs:11:14
   |
11 |   fn example(foo: Foo) {
   |              ^^^ doesn't have a size known at compile-time
   |
   = help: within `upstream::Foo`, the trait `Sized` is not implemented for `[u8]`
   = note: required because it appears within the type `upstream::Foo`
help: function arguments must have a statically known size, borrowed types always have a known size
   |
11 |   fn example(&foo: Foo) {
   |              ^

Changing the size of a Sized type can also break (poorly-behaving) downstream code. The mem::size_of intrinsic is a safe function that provides the size (in bytes) of any Sized type. By convention, downstream code should not rely on mem::size_of producing a SemVer stable result, but that's only a convention. Consider:

pub mod upstream {
  #[repr(C)]
  pub struct Foo {
    bar: u8,
    // uncommenting this field is a breaking change for `downstream`:
    /* baz: u8 */
  }
}

pub mod downstream {
  use super::upstream::*;
  
  const _: [(); 1] = [(); std::mem::size_of::<Foo>()];
}
error[E0308]: mismatched types
  --> src/lib.rs:12:22
   |
12 |   const _: [(); 1] = [(); std::mem::size_of::<Foo>()];
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected an array with a fixed size of 1 element, found one with 2 elements

Zero Sizedness

A downstream crate author doesn't only need to worry that they aren't using mem::size_of in a manner that breaks the stability contract of upstream code. As of 2018, there's another mechanism that observes the size of a type: #[repr(transparent)].

The repr(transparent) attribute can be applied to types with at most one non-zero-sized field to specify that the annotated type's layout is identical to that of the field. Applying repr(transparent) to a type with more than one non-zero-sized field is a compiler error:

#[repr(transparent)]
pub struct Foo {
    bar: u8,
    baz: u8
}
error[E0690]: transparent struct needs exactly one non-zero-sized field, but has 2
 --> src/lib.rs:2:1
  |
2 | pub struct Foo {
  | ^^^^^^^^^^^^^^ needs exactly one non-zero-sized field, but has 2
3 |     bar: u8,
  |     ------- this field is non-zero-sized
4 |     baz: u8
  |     ------- this field is non-zero-sized

Consequently, upstream changes that turn ZSTs into non-ZSTs can break downstream code.

pub mod upstream {
  #[repr(C)]
  pub struct Foo {
    bar: (),
    // uncommenting this field is a breaking change for `downstream`:
    /* baz: u8, */
  }
}

pub mod downstream {
  use super::upstream::*;

  #[repr(transparent)]
  struct Bar(u8, Foo);
}
error[E0690]: transparent struct needs exactly one non-zero-sized field, but has 2
  --> src/lib.rs:12:3
   |
12 |   struct Bar(u8, Foo);
   |   ^^^^^^^^^^^--^^---^^
   |   |          |   |
   |   |          |   this field is non-zero-sized
   |   |          this field is non-zero-sized
   |   needs exactly one non-zero-sized field, but has 2

You should therefore avoid #[repr(transparent)] unless the ZST field types are documented to remain ZSTs.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK