Typecasts
Rust provides no implicit type conversion (coercion) between primitive types. This is a deliberate design choice to improve safety and avoid unexpected behavior.
Instead of "casting," you'll usually implement or use traits like From
, Into
, TryFrom
, TryInto
, or FromStr
for type conversions in Rust. The as
keyword exists, but it should be used carefully and only when other options aren't suitable, as it can lead to data loss or unexpected behavior if not handled properly.
Type Conversion using as
The as
keyword is used for basic type conversions, but it's important to be aware of potential issues like truncation or overflow. This is the closest equivalent to C-style casting, but it should be used with caution.
Conversion Traits: From
, Into
, TryFrom
and TryInto
The From
and Into
traits are used for conversions that should always succeed. From
allows you to define how to convert from a type, and Into
provides a blanket implementation for converting into a type if From is implemented. This is the preferred way to do type conversions in most cases.
The TryFrom
and TryInto
traits are used for conversions that might fail. They return a Result
to indicate success or failure. Use these when there's a possibility of the conversion not working (e.g., parsing a string to a number).
Parsing Strings with the FromStr
Trait
The FromStr
trait is used for parsing strings into other types. Many standard types implement FromStr
.
let num: i32 = "123".parse().unwrap();
Casting Between Numeric Types
Use the as
keyword, but be very cautious about potential loss of data or unexpected behavior due to truncation or overflow.
Casting Between Pointers
Pointer casting requires unsafe
blocks and careful consideration of memory management.
Polymorphism with Traits (Dynamic Dispatch)
Use trait objects (dyn Trait
) for dynamic dispatch.
Coercions (Implicit Conversions)
Rust performs some implicit coercions, such as dereferencing and unsizing. These are not type casts in the traditional sense, but they do involve implicit changes in type.
bytemuck
// COMING SOON
zerocopy
zerocopy
⮳ makes zero-cost memory manipulation safe. It provides a set of traits and utilities to work with types that can be safely interpreted as byte slices.
- No data copying: Zero-copy avoids unnecessary data copying by directly interpreting the memory of one data structure as another.
- Performance: Eliminating data copying can significantly improve performance, especially in scenarios involving frequent data transfers between different memory regions (e.g., network I/O, inter-process communication).
- Safety: The
zerocopy
⮳ crate provides mechanisms to ensure safe and correct zero-copy operations.
Zerocopy is often used in network programming, where high performance and low memory overhead are critical, or image handling.
// For performance reasons, CPUs often strongly prefer or even mandate storing // data in memory at addresses that are multiples of the word size (usually 4 or // 8 bytes). For example, on a x86 architecture, u64 and f64 are often aligned // to 4 bytes (32 bits). // // In order to not waste space while still respecting the CPU's preferred // alignment, the Rust compiler optimizes the layout of composite data // structures (e.g. structs, tuples, arrays, enums...) when storing them in // memory. It may reorder their fields, and may insert "gaps" of one or more // bytes (often called padding) before, between, and after the fields. // As a result, structs, enums... can't, in general, be treated as contiguous // blocks of bytes. // // However, some can, given proper restrictions on layout (aka "representation") // and field types. Zerocopy performs a sophisticated, compile-time safety // analysis to determine whether such zero-cost, zero-copy conversions are safe. // See e.g. https://docs.rs/zerocopy/0.8.14/zerocopy/derive.IntoBytes.html#analysis // // For that purpose, Zerocopy provides several derivable traits: // // `FromBytes` indicates that a type may safely be converted from an // arbitrary byte sequence This is useful for efficiently deserializing // structured data from raw bytes. Do not implement this trait yourself! // Instead, derive it, as we do below. use zerocopy::FromBytes; // There is also a `TryFromBytes` trait for types that may safely be // converted from certain byte sequences (conditional on runtime checks) // // `FromZeros` indicates that a sequence of zero bytes represents a valid // instance of a type. use zerocopy::FromZeros; // Marker trait that flags types which are free from interior mutability. // Required to call certain methods provided by the conversion traits: use zerocopy::Immutable; // `IntoBytes` indicates that a type may safely be converted to a byte // sequence initialized bytes of the same size. // This is useful for efficiently serializing structured data as raw bytes. // Do not implement this trait yourself! Instead, derive it, as we did // above. use zerocopy::IntoBytes; // Marker trait that indicates that zerocopy can reason about certain // aspects of a type's layout. This trait is required by many of zerocopy's // APIs. use zerocopy::KnownLayout; // Add the following to your `Cargo.toml`: // zerocopy = { version = "0.8.14", features = ["derive"] } # or latest version // Let's first demonstrate `FromZeros`: #[derive(FromZeros, Debug)] struct MyZeroableStruct { field: [u8; 8], } fn manipulate_zero_bytes() { // Creates an instance from zeroed bytes. let my_struct: MyZeroableStruct = FromZeros::new_zeroed(); assert_eq!(my_struct.field, [0; 8]); let mut my_struct2 = MyZeroableStruct { field: [1; 8] }; println!("{:?}", my_struct2); // Sets every byte in self to 0. While this is similar to doing // *self = Self::new_zeroed(), it differs in that zero does not semantically // drop the current value and replace it with a new one - it simply // modifies the bytes of the existing value. my_struct2.zero(); assert_eq!(my_struct2.field, [0; 8]); } // We then define a fictive network PacketHeader structure. // As discussed above, all user-defined composite types (structs, enums, // unions...) have a representation that specifies what the layout (its size, // alignment, and the order / relative offsets of its fields) is for the type: https://doc.rust-lang.org/reference/type-layout.html#representations // Types you expect to pass through an FFI / network boundary most often are // most often `repr(C)`, as C is the lingua-franca of the programming world. It // means that their layout is exactly that C or C++ expect. #[repr(C)] // We derive the Zerocopy traits that are required to convert from / to bytes. #[derive(FromBytes, IntoBytes, Immutable, KnownLayout, PartialEq, Debug)] struct PacketHeader { src_port: [u8; 2], dst_port: [u8; 2], length: [u8; 2], checksum: [u8; 2], } // Define a `Packet` struct. // Note that, in this case, the Packet's `body` is a slice, which can have // different lengths at runtime. Zerocopy can handle a "slice-based dynamically // sized type". A slice DST is a type whose trailing field is either a slice or // another slice DST, rather than a type with fixed size. #[repr(C)] #[derive(FromBytes, Immutable, KnownLayout)] struct Packet { header: PacketHeader, body: [u8], } // Convert the PacketHeader into bytes fn into_bytes() -> anyhow::Result<()> { let mut header = PacketHeader { src_port: [0, 1], dst_port: [2, 3], length: [4, 5], checksum: [6, 7], }; let bytes: &mut [u8] = header.as_mut_bytes(); // There is also an `as_bytes` method. assert_eq!(bytes, [0, 1, 2, 3, 4, 5, 6, 7]); // Note that `bytes` and `header` share the same memory bytes.reverse(); assert_eq!( header, PacketHeader { src_port: [7, 6], dst_port: [5, 4], length: [3, 2], checksum: [1, 0], } ); // You can also write a copy to the destination byte array. // If too many or too few target bytes are provided, `write_to` returns // `Err` and leaves the target bytes unmodified. let mut buf = [0, 0, 0, 0, 0, 0, 0, 0]; header .write_to(&mut buf[..]) .map_err(|_| anyhow::anyhow!("write_to error!"))?; assert_eq!(buf, [7, 6, 5, 4, 3, 2, 1, 0]); // There are also methods to write to the beginning or end of a byte buffer // or to IO. Ok(()) } // Convert bytes into a Packet fn from_bytes() -> anyhow::Result<()> { // These bytes encode a `Packet`. let bytes: &[u8] = &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11][..]; let packet = Packet::ref_from_bytes(bytes)?; assert_eq!(packet.header.src_port, [0, 1]); assert_eq!(packet.header.dst_port, [2, 3]); assert_eq!(packet.header.length, [4, 5]); assert_eq!(packet.header.checksum, [6, 7]); // The bytes beyond the 8th are the body: assert_eq!(packet.body, [8, 9, 10, 11]); Ok(()) } // Enums can also be used. // An enum can implement `IntoBytes`, if it has // - a defined representation (reprs C, u8, u16, u32, u64, usize, i8, i16, i32, // i64, or isize). // - no padding bytes; and // - Its fields must be `IntoBytes`. // Here, the enum is field-less. It stores 0 for A, 1 for B, etc... in a byte. #[repr(u8)] #[derive(IntoBytes, Immutable)] enum MyEnum { #[allow(dead_code)] A, B, } fn enums_also_work() { assert_eq!(size_of::<MyEnum>(), 1); let my_enum = MyEnum::B; let bytes = my_enum.as_bytes(); assert_eq!(bytes, [1]); } // Safely transmutes a value of one type // to a value of another type of the same size. fn transmute() { let one_dimensional: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7]; let two_dimensional: [[u8; 4]; 2] = zerocopy::transmute!(one_dimensional); assert_eq!(two_dimensional, [[0, 1, 2, 3], [4, 5, 6, 7]]); } fn main() -> anyhow::Result<()> { manipulate_zero_bytes(); into_bytes()?; from_bytes()?; enums_also_work(); transmute(); Ok(()) } // Examples adapted from https://docs.rs/zerocopy/
Key Differences from C/C++
- Rust is much more explicit about type conversions. This helps to avoid bugs and makes the code more readable.
- Rust encourages the use of traits like
From
,Into
,TryFrom
, andTryInto
for conversions. This makes the code more generic and reusable. - Rust avoids implicit type casting, which can lead to unexpected behavior in C/C++.