| |

Rust 119 🦀 Zero-Copy Parsing and Lifetime-Bound Deserialization (zerocopy / rkyv)

⏱️ Estimated read: 12 min | |

Traditional deserialization reads a byte buffer, parses its contents, and allocates new memory to reconstruct the data structure. Zero-copy deserialization skips that final step: the byte buffer is laid out so that it already is the in-memory representation, and the program simply casts a pointer into the buffer and reads fields directly. Rust’s type system makes this safe through two distinct approaches. The zerocopy crate provides marker traits that guarantee a type can be safely reinterpreted from arbitrary bytes. The rkyv framework goes further, laying out entire data structures so that an archived buffer is a valid, aligned instance of the archived type, requiring no parsing at all . This chapter covers both approaches, the safety contracts they enforce, and the lifetime-bound types that make zero-copy access ergonomic.

Key point: Zero-copy deserialization does not eliminate the need to validate data; it moves validation to the type system. The buffer is either a valid instance of the type (checked once at access time), or it is rejected. After validation, field access is a pointer offset, not a parse.


Why zero-copy parsing exists

The allocation cost problem. Deserializing a JSON array of a thousand strings allocates a Vec, a String for each element, and copies every byte. For data-heavy applications—games, media editors, network services handling large payloads—this allocation and copying dominates load time . Zero-copy deserialization eliminates the allocation entirely by borrowing from the source buffer.

The layout problem. A &[u8] is just bytes. Reinterpreting it as a u32 requires knowing that the bytes are valid for a u32, that they are aligned correctly, and that the byte order matches the platform. Without compile-time guarantees, this is unsafe code that is easy to get wrong. The zerocopy crate provides marker traits—FromBytes, AsBytes, Unaligned—that encode these guarantees in the type system, allowing safe conversion with zero runtime overhead .

The archive problem. Even with safe byte reinterpretation, complex types like String and Vec contain pointers. A buffer containing a String cannot be directly reinterpreted because the pointer inside it points to memory that does not exist at the new location. rkyv solves this by defining a separate “archived” representation where pointers are replaced with relative offsets. The archived type has the same layout as the original, but its pointer fields are meaningful when interpreted relative to the buffer’s base address . Accessing an archived String does not copy the string; it computes an offset and returns a reference into the buffer.

The validation problem. A buffer might be corrupted, truncated, or maliciously crafted. rkyv provides optional validation that checks the archive’s internal offsets and types before allowing access. Validation is allocation-free and runs in linear time, but it can be skipped for trusted data using the unchecked API .

The trade-off. Zero-copy access is fast, but it couples the in-memory representation to the serialized format. Changing the type’s layout changes the archive format, breaking compatibility with previously serialized data. Traditional serialization (JSON, Bincode) abstracts the format from the memory layout, allowing schema evolution at the cost of parsing overhead.


a. The zerocopy marker traits

The zerocopy crate provides four core marker traits that encode safety invariants at compile time . FromBytes indicates that any byte sequence is a valid instance of the type. AsBytes indicates that the type can be safely converted to bytes. FromZeroes indicates that all-zero bytes represent a valid instance (useful for Option<NonZeroU32> and similar types). Unaligned indicates that the type’s alignment requirement is 1, so it can be placed anywhere in a buffer.

use zerocopy::{FromBytes, AsBytes, Unaligned};

#[derive(FromBytes, AsBytes, Unaligned)]
#[repr(C)]
struct PacketHeader {
    version: u8,
    flags: u8,
    length: u16,
}

Deriving these traits is safe because the compiler verifies the type’s layout. FromBytes is only valid for types where every possible byte pattern is a valid value—primitive integers, arrays of FromBytes types, and structs whose fields are all FromBytes. Types with invariants (like bool, which must be 0 or 1) cannot implement FromBytes because a random byte might be 2. For such types, FromZeroes provides a weaker guarantee: all-zero bytes are valid, but not arbitrary bytes .

The conversion methods on FromBytes are where the zero-copy access happens. ref_from_bytes interprets a &[u8] as a &Self without copying. read_from_bytes copies a Self out of the buffer. mut_from_bytes provides a mutable reference, allowing in-place modification of the buffer through the typed view . The alignment requirement is enforced at runtime: if the buffer is not correctly aligned for Self, the method returns an error.


b. rkyv archived types and zero-copy access

rkyv takes a different approach. Instead of requiring the original type to be byte-compatible, it generates a parallel Archived type for every serializable type. The Archive derive macro produces a type with the same field names and structure, but with pointer fields replaced by RelPtr and ArchivedString / ArchivedVec .

use rkyv::{Archive, Deserialize, Serialize};

#[derive(Archive, Serialize, Deserialize)]
struct Player {
    name: String,
    level: u32,
    scores: Vec<u32>,
}

The Archive derive generates ArchivedPlayer with fields name: ArchivedString, level: ArchivedU32, and scores: ArchivedVec<ArchivedU32>. Serializing a Player with rkyv::to_bytes produces an AlignedVec containing the archived representation. Accessing the data requires only a pointer cast: rkyv::access::<ArchivedPlayer, _>(&bytes) validates the buffer (if validation is enabled) and returns a reference that points directly into it .

let bytes = rkyv::to_bytes::<Error>(&player)?;
let archived = rkyv::access::<ArchivedPlayer, Error>(&bytes)?;
let total: u32 = archived.scores.iter().map(|s| s.to_native()).sum();

Iterating over archived.scores does not copy the vector. Each element is an ArchivedU32, and to_native() performs the byte-order conversion. The string archived.name is an ArchivedString, which dereferences to &str without allocation. The entire access pattern is pointer arithmetic into the original buffer .

The lifetime connection is implicit: archived borrows from bytes, and the type system ensures that the buffer outlives the archived reference. rkyv::access returns &ArchivedPlayer, not an owned value. To obtain an owned Player, rkyv::deserialize must be called, which performs the full copy and allocation . The choice between zero-copy access and full deserialization is explicit in the API.


c. Lifetime-bound types and validation

Both approaches tie the lifetime of the accessed data to the lifetime of the source buffer. In zerocopy, ref_from_bytes returns &'_ Self where the lifetime is that of the input slice. In rkyv, access returns &ArchivedPlayer borrowing from the AlignedVec. The compiler prevents using the archived data after the buffer is dropped.

let bytes = rkyv::to_bytes::<Error>(&player)?;
let archived = rkyv::access::<ArchivedPlayer, Error>(&bytes)?;
drop(bytes); // error: cannot drop bytes while borrowed
println!("{}", archived.name);

The validation step is optional in rkyv. The check_bytes function verifies that the archive’s internal offsets are within bounds and that the discriminant of any enums is valid. If validation is skipped (using access_unchecked), the user asserts that the buffer is trusted. For data from untrusted sources, validation is mandatory—a corrupted offset could cause a read outside the buffer’s bounds, which is undefined behavior .

The zerocopy crate’s safety model is different. It does not validate the contents of the buffer; it validates the type. If a type implements FromBytes, any byte sequence is a valid instance of that type. The responsibility for ensuring that the bytes are meaningful (e.g., a version number is not 255) lies with the application. This is a weaker guarantee than rkyv provides, but it is sufficient for primitive types and fixed-layout headers where every bit pattern is a valid value.


Complete Example Session

# ============================================
# PART 1: DEFINING A ZEROCOPY TYPE
# ============================================

use zerocopy::{FromBytes, AsBytes, Unaligned};

#[derive(FromBytes, AsBytes, Unaligned)]
#[repr(C)]
struct MessageHeader {
    msg_type: u8,
    flags: u8,
    payload_len: u16,
}
# ============================================
# PART 2: SAFE REINTERPRETATION
# ============================================

let buffer = [0x01, 0x02, 0x00, 0x10];
let header = MessageHeader::ref_from_bytes(&buffer).unwrap();
// header.msg_type == 1, header.payload_len == 4096
# ============================================
# PART 3: THE RKYV ARCHIVE TYPE
# ============================================

use rkyv::{Archive, Serialize, Deserialize};

#[derive(Archive, Serialize, Deserialize)]
struct Document {
    title: String,
    body: String,
    views: u64,
}
# ============================================
# PART 4: SERIALIZATION
# ============================================

let doc = Document {
    title: "Zero Copy".into(),
    body: "The buffer is the data.".into(),
    views: 42,
};
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&doc)?;
# ============================================
# PART 5: ZERO-COPY ACCESS
# ============================================

let archived = rkyv::access::<ArchivedDocument, rkyv::rancor::Error>(&bytes)?;
// archived.title is &str, no allocation
println!("Title: {}", archived.title);
# ============================================
# PART 6: VALIDATION
# ============================================

// access() validates by default.
// For trusted data, skip validation:
let unchecked = unsafe {
    rkyv::access_unchecked::<ArchivedDocument>(&bytes)
};
// Safety: bytes is trusted and not modified.
# ============================================
# PART 7: OWNED DESERIALIZATION
# ============================================

let owned: Document = rkyv::deserialize::<Document, _>(archived)?;
// owned.title is String, allocated and copied.
# ============================================
# PART 8: IN-PLACE MUTATION (RKYV)
# ============================================

let mut bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&doc)?;
let archived = rkyv::access_mut::<ArchivedDocument, _>(&mut bytes)?;
archived.views = rkyv::rend::u64_le::from(100);
# ============================================
# PART 9: LIFETIME BINDING
# ============================================

// archived borrows from bytes.
// Compiler prevents using archived after bytes is dropped.
let _ = rkyv::access::<ArchivedDocument, _>(&bytes)?;
drop(bytes); // would be an error if archived were still live

The nine parts trace the full lifecycle: defining a zerocopy type, safe reinterpretation, defining an rkyv archive type, serialization, zero-copy access, validation, owned deserialization, in-place mutation, and lifetime binding. Each step demonstrates a concrete operation in the zero-copy workflow.


Quick Reference

zerocopy Marker Traits

TraitGuarantee
FromBytesAny byte sequence is a valid instance
AsBytesCan be safely converted to bytes
FromZeroesAll-zero bytes are valid
UnalignedAlignment requirement is 1

rkyv Operations

OperationFunctionAllocation
Serializerkyv::to_bytesOne AlignedVec
Validaterkyv::accessNone
Unchecked accessaccess_uncheckedNone
Deserializerkyv::deserializeFull reconstruction

Zero-Copy Comparison

CrateApproachValidationMutability
zerocopyType-level reinterpretationNone (type-enforced)Mutable with mut_from_bytes
rkyvArchive layout with relative offsetsOptional (access vs access_unchecked)In-place with access_mut

Best Practices

✅ Do This:

#[derive(FromBytes, Unaligned)]
#[repr(C)]
struct Header { version: u8, length: u16 }
  • ✅ Derive marker traits for fixed-layout types
let archived = rkyv::access::<ArchivedDoc, _>(&bytes)?;
  • ✅ Validate untrusted buffers before access
let owned: Doc = rkyv::deserialize(archived)?;
  • ✅ Deserialize only when owned data is required

❌ Don’t Do This:

#[derive(FromBytes)]
struct Bad { value: bool }
  • ❌ bool cannot implement FromBytes (byte 2 is invalid)
let archived = unsafe { rkyv::access_unchecked(&untrusted_bytes) };
  • ❌ Skipping validation on untrusted data is undefined behavior
let header = Header::ref_from_bytes(&buffer[1..]).unwrap();
  • ❌ Misaligned buffer will fail or cause UB

Common Pitfalls

PitfallWhy It HappensFix
Misaligned access&[u8] from Vec has alignment 1Copy into AlignedVec or use Unaligned
FromBytes on boolNot every byte is a valid boolUse a u8 and convert
Validation skippedPerformance optimization on untrusted dataAlways validate; use access_unchecked only for trusted buffers
Archive format changeLayout change breaks old buffersPin the rkyv version or version the format
Lifetime escapeTrying to return archived data without owning bufferReturn AlignedVec and Archived together, or deserialize

Real-World Examples

1. Network Header Parsing

let header = TcpHeader::ref_from_prefix(&packet)?;

2. Game Save File

let archived = rkyv::access::<ArchivedWorld, _>(&save_bytes)?;

3. High-Score Table

for entry in archived.scores.iter() {
    println!("{}", entry.to_native());
}

4. In-Place Counter Update

let archived = rkyv::access_mut::<ArchivedStats, _>(&mut buf)?;
archived.hits = 10.into();

5. Zero-Copy String Slice

let title: &str = &archived.title;

6. Fixed-Layout Config

let cfg = Config::read_from_bytes(&file)?;

7. Validated Archive

let archived = rkyv::access::<ArchivedMsg, _>(&untrusted)?;

8. Deserialize for Mutation

let mut owned: Msg = rkyv::deserialize(archived)?;
owned.count += 1;

9. Mmap Access

let mmap = unsafe { Mmap::map(&file)? };
let archived = rkyv::access(&mmap[..])?;

10. Unaligned Packet

let header = Unaligned::<Header>::ref_from_bytes(&buf)?;

Visual

Traditional Deserialization vs Zero-Copy

┌──────────────────────────────────────────────┐
│  Traditional (JSON/Bincode)                  │
│                                              │
│  Buffer → Parse → Allocate → New Structure   │
│                                              │
│  Copy: YES. Allocation: YES.                 │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│  Zero-Copy (rkyv / zerocopy)                 │
│                                              │
│  Buffer → Validate → Cast → Read Fields      │
│                                              │
│  Copy: NO. Allocation: NO.                   │
└──────────────────────────────────────────────┘

The buffer is the data. Access is a pointer offset, not a reconstruction.

rkyv Archive Layout

┌──────────────────────────────────────────────┐
│  Buffer                                      │
│                                              │
│  ┌────────────┐                              │
│  │ name:      │                              │
│  │ RelPtr     │──────┐                       │
│  │ len: u32   │      │                       │
│  ├────────────┤      │                       │
│  │ level: u32 │      │                       │
│  ├────────────┤      │                       │
│  │ scores:    │      │                       │
│  │ RelPtr     │──┐   │                       │
│  └────────────┘  │   │                       │
│                  │   ▼                       │
│                  │  ┌──────────────────────┐  │
│                  │  │ "Ferris" (bytes)     │  │
│                  │  └──────────────────────┘  │
│                  ▼                           │
│                ┌──────────────────────────┐  │
│                │ [100, 250, 75]           │  │
│                └──────────────────────────┘  │
└──────────────────────────────────────────────┘

Relative pointers (RelPtr) store offsets from their own location. Accessing name adds the offset to the pointer’s address, yielding a slice into the same buffer.

Validation Boundary

┌──────────────────────────────────────────────┐
│  Untrusted buffer                            │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  rkyv::access()                              │
│                                              │
│  Check offsets are within bounds             │
│  Check discriminants are valid               │
│  Check alignment                             │
└──────────┬───────────────────────┬───────────┘
           │ VALID                 │ INVALID
           ▼                       ▼
┌──────────────────────┐  ┌──────────────────────┐
│  &ArchivedType       │  │  Err                 │
│  Zero-copy access    │  │  Reject buffer       │
└──────────────────────┘  └──────────────────────┘

Validation is the boundary between untrusted bytes and safe access. access_unchecked skips this boundary and must be used only for trusted buffers.


Summary

ItemValue
zerocopy traitsFromBytes, AsBytes, FromZeroes, Unaligned
zerocopy accessref_from_bytes, mut_from_bytes
rkyv deriveArchive generates ArchivedType
rkyv serializationrkyv::to_bytes → AlignedVec
rkyv zero-copy accessrkyv::access::<ArchivedType, _>
rkyv validationBuilt into access; access_unchecked skips
rkyv owned copyrkyv::deserialize::<T, _>
Lifetime bindingArchived reference borrows from buffer
Mutationzerocopy: mut_from_bytes; rkyv: access_mut

Key takeaways:

  • zerocopy provides safe byte reinterpretation. The marker traits guarantee that a type can be viewed as bytes and vice versa, with alignment checked at runtime .
  • rkyv provides total zero-copy deserialization. The archived layout matches the in-memory layout, so access is a pointer cast, not a parse .
  • Validation is the safety boundary. rkyv::access validates untrusted buffers before returning a reference. access_unchecked skips validation and must be used only for trusted data .
  • Lifetimes prevent use-after-free. Archived data borrows from the source buffer; the compiler ensures the buffer outlives the reference.
  • Owned deserialization is opt-in. When mutation or ownership is required, rkyv::deserialize performs the full allocation and copy .
  • Layout is the format. Changing a type’s layout changes the serialized format. Version the format or pin the library version when persistence matters.

Remember: Zero-copy parsing does not eliminate validation; it moves it. zerocopy validates at the type level, ensuring that the byte pattern is valid for the type. rkyv validates at the archive level, ensuring that pointers are in bounds and discriminants are valid. After validation, access is a pointer offset into the original buffer. The buffer is not copied, not allocated, and not parsed. It is read in place.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!