Rust 119 🦀 Zero-Copy Parsing and Lifetime-Bound Deserialization (zerocopy / rkyv)
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
| Trait | Guarantee |
|---|---|
FromBytes | Any byte sequence is a valid instance |
AsBytes | Can be safely converted to bytes |
FromZeroes | All-zero bytes are valid |
Unaligned | Alignment requirement is 1 |
rkyv Operations
| Operation | Function | Allocation |
|---|---|---|
| Serialize | rkyv::to_bytes | One AlignedVec |
| Validate | rkyv::access | None |
| Unchecked access | access_unchecked | None |
| Deserialize | rkyv::deserialize | Full reconstruction |
Zero-Copy Comparison
| Crate | Approach | Validation | Mutability |
|---|---|---|---|
zerocopy | Type-level reinterpretation | None (type-enforced) | Mutable with mut_from_bytes |
rkyv | Archive layout with relative offsets | Optional (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 }
- ❌
boolcannot implementFromBytes(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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Misaligned access | &[u8] from Vec has alignment 1 | Copy into AlignedVec or use Unaligned |
FromBytes on bool | Not every byte is a valid bool | Use a u8 and convert |
| Validation skipped | Performance optimization on untrusted data | Always validate; use access_unchecked only for trusted buffers |
| Archive format change | Layout change breaks old buffers | Pin the rkyv version or version the format |
| Lifetime escape | Trying to return archived data without owning buffer | Return 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
| Item | Value |
|---|---|
zerocopy traits | FromBytes, AsBytes, FromZeroes, Unaligned |
zerocopy access | ref_from_bytes, mut_from_bytes |
rkyv derive | Archive generates ArchivedType |
rkyv serialization | rkyv::to_bytes → AlignedVec |
rkyv zero-copy access | rkyv::access::<ArchivedType, _> |
rkyv validation | Built into access; access_unchecked skips |
rkyv owned copy | rkyv::deserialize::<T, _> |
| Lifetime binding | Archived reference borrows from buffer |
| Mutation | zerocopy: mut_from_bytes; rkyv: access_mut |
Key takeaways:
zerocopyprovides safe byte reinterpretation. The marker traits guarantee that a type can be viewed as bytes and vice versa, with alignment checked at runtime .rkyvprovides 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::accessvalidates untrusted buffers before returning a reference.access_uncheckedskips 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::deserializeperforms 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.
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!