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
//! The `bytemuck` crate provides functions for reinterpreting one type as //! another without use of `unsafe`, whenever this is safe e.g. because both //! types are "just bytes" (i.e. no invalid values and no padding). //! //! - To cast `T`, use `try_cast` or `cast`. //! - For `&T`, use `(try_)cast_ref`. //! - For `&mut T`, use `(try_)cast_mut`. //! - For `&[T]`, use `(try_)cast_slice`. //! - For `&mut [T]`, use `(try_)cast_slice_mut`. //! //! Add to your `Cargo.toml`: //! ```toml //! bytemuck = { version = "1.21.0", features = ["derive"] } //! ``` //! //! The crate offers a number of additional features to work with allocated //! types (`Box`, `Vec`...), `Zeroable` types, SIMD types, and to ensure at //! compile time that a cast will work at runtime. //! //! It also offers casting for types that have some invalid bit patterns by //! performing a runtime check. This is particularly useful for types like //! fieldless (‘C-style’) enums, char, bool, and structs containing them. use bytemuck::AnyBitPattern; use bytemuck::NoUninit; /// Cast type A into type B, here an array into another. fn cast() { let sixteens: [u16; 4] = [1, 2, 3, 4]; println!("Sixteens: {:?}", sixteens); // `cast` is purely changing the type, thus have no run-time cost. // - It does not reorder bytes to a specific endianness. // - It will panic on a size mismatch. let eights: [u8; 2 * 4] = bytemuck::cast(sixteens); println!("Eights: {:?}", eights); } /// You can also cast complex types, like a `struct`, provided that it has been /// properly annotated. /// /// The `NoUninit` and `AnyBitPattern` traits are used to maintain memory /// safety. `NoUninit` is a marker trait for "plain old data" types with no /// (uninitialized) padding bytes. `AnyBitPattern` marks "plain old data" types /// that are valid for any bit pattern. #[repr(C)] #[derive(Debug, Clone, Copy, NoUninit, AnyBitPattern)] struct MyStruct { x: i32, y: [u8; 4], } /// Cast the `struct` to an array. fn cast_complex_type() { // Create an instance of `MyStruct`. let my_struct = MyStruct { x: 42, y: [1, 2, 3, 4], }; println!("\n{:?}", my_struct); let my_ref = &my_struct; let r: Result<&[i32; 2], _> = bytemuck::try_cast_ref(my_ref); println!("{:?}", r); } /// You can also re-interpret `&T` as `&[u8]` and vice-versa. fn to_from_bytes() { let my_struct = MyStruct { x: 42, y: [1, 2, 3, 4], }; // `bytes_of` re-interprets `&T` as `&[u8]`, but again only if `T` is // `NoUninit` that is "plain old data" types with no uninit (or padding) // bytes. let bytes: &[u8] = bytemuck::bytes_of(&my_struct); // Print the bytes: [42, 0, 0, 0, 1, 2, 3, 4] (on a little-endian machine). println!("Bytes: {:?}", bytes); // Re-interprets `&[u8]` as `&T`, but only if `T` is `AnyBitPattern`, i.e. // "plain old data" types that are valid for any bit pattern. let recovered: &MyStruct = bytemuck::from_bytes(bytes); println!("Recovered: {:?}", recovered); } fn main() { cast(); cast_complex_type(); to_from_bytes(); }
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.
//! This example demonstrates zero-cost memory manipulation with `zerocopy`. //! //! First, add the following to your `Cargo.toml`: //! ```toml //! zerocopy = { version = "0.8.14", features = ["derive"] } # or latest version //! ``` //! //! 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`, //! `TryFromBytes`, `FromZeros`, `Immutable`... use std::mem::size_of; // `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; // `Immutable` is a marker trait that flags types which are free from // interior mutability. use zerocopy::Immutable; // `Immutable` is required to call certain methods provided by the // conversion traits: `IntoBytes` indicates that a type may safely be // converted to a byte sequence of 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. use zerocopy::IntoBytes; // `KnownLayout` is a marker trait that indicates that zerocopy can reason // about certain aspects of a type's layout. use zerocopy::KnownLayout; // 1. 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]); } // 2. 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(); // Note: 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 a 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(()) } // 3. 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 /// - `IntoBytes` fields. /// /// 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]); } /// 4. 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++.