Как перестать бояться unsafe в Rust и подружиться с памятью напрямую
Представьте, что вы пишете сетевой протокол или парсер бинарных форматов на Rust. У вас есть пачка байтов, прилетевшая из сокета, и вы точно знаете, что это структура заголовка. В языке Си вы бы просто привели указатель: (Header*)buffer. В Rust такой фокус заставит компилятор ругаться, а использование unsafe и std::mem::transmute превратит код в минное поле. Один неверный сдвиг, и вы ловите undefined behavior из-за проблем с выравниванием (alignment) или невалидных данных.
Команда Google столкнулась с этой проблемой при разработке Fuchsia OS и создала библиотеку zerocopy. Она делает ровно то, что обещано в названии: позволяет интерпретировать байты как структуры и наоборот без копирования, лишних аллокаций и, что самое приятное, без написания unsafe вручную.
В чем подвох с ручным приведением типов
Когда мы пытаемся превратить &[u8] в &MyStruct, мы берем на себя огромную ответственность. Нужно гарантировать, что:
- Размер слайса байтов совпадает с размером структуры.
- Данные в памяти выровнены правильно (например,
u32должен начинаться по адресу, кратному 4). - Любая комбинация битов в этом буфере является валидным значением для вашего типа.
Если вы ошибетесь хоть в одном пункте, программа может упасть или, что хуже, продолжить работать с «протухшими» данными. Zerocopy перекладывает эти проверки на систему типов Rust.
Как это работает на практике
Библиотека предоставляет набор трейтов, которые вы вешаете на свои структуры. Самый простой способ — использовать дерайв-макросы.
use zerocopy::{AsBytes, FromBytes, FromZeroes};
#[derive(AsBytes, FromBytes, FromZeroes)]
#[repr(C)]
struct NetworkPacket {
id: u32,
payload_len: u16,
flags: u8,
}
Тут важно заметить #[repr(C)]. Без него Rust волен переставлять поля структуры как ему вздумается, что недопустимо для бинарных протоколов.
После того как вы пометили структуру, работа с ней становится элементарной:
let bytes = [0x01, 0x00, 0x00, 0x00, 0x05, 0x00, 0xFF];
// Просто читаем структуру из байтов
if let Some(packet) = NetworkPacket::read_from(&bytes[..]) {
println!("ID: {}, Len: {}", packet.id, packet.payload_len);
}
// И так же легко превращаем обратно в байты для отправки
let raw_bytes = packet.as_bytes();
Никаких циклов, никаких промежуточных буферов. Вы просто смотрите на ту же самую память под другим углом.
Почему это безопасно
Zerocopy не просто «скрывает» unsafe под капотом. Макросы проверяют вашу структуру во время компиляции. Если вы попытаетесь реализовать FromBytes для типа, который содержит «дырки» (padding) или типы, которые не могут быть заполнены любым набором байтов (например, bool или enum), компилятор выдаст ошибку.
Например, bool в Rust может быть только 0 или 1. Если в байте окажется 2, это UB. Zerocopy не даст вам автоматически привести байты к структуре с bool, защищая от подобных сюрпризов.
Где это пригодится
Я часто вижу, как разработчики используют serde для парсинга бинарщины. Это удобно, но serde всегда создает новый объект в памяти. Если вы пишете высоконагруженный прокси-сервер или драйвер, где важна каждая микросекунда, zerocopy — ваш выбор.
Кейсы, где библиотека раскрывается лучше всего:
- Написание сетевых стеков (TCP/IP, кастомные протоколы).
- Работа с файловыми системами и заголовками файлов.
- Взаимодействие с оборудованием через memory-mapped I/O.
- Передача данных между Rust и C/C++ без оверхеда на сериализацию.
Стоит ли тащить это в проект
Если ваш проект — типичный веб-сервис на JSON, то zerocopy вам, скорее всего, не нужен. Но если вы работаете «ближе к железу» или боретесь за производительность парсинга, это мастхэв.
Библиотека уже достаточно зрелая, её активно используют в проектах Google (та же Fuchsia). Из минусов можно отметить разве что строгие требования к структурам — иногда приходится возиться с выравниванием вручную, чтобы макросы «проглотили» ваш тип. Но это честная цена за гарантию того, что ваше приложение не упадет из-за ошибки сегментации в самый неподходящий момент.
Начать изучение лучше всего с документации на docs.rs, там подробно расписано, какой трейт за что отвечает. И помните: лучший unsafe — это тот, который написал кто-то другой и покрыл его тестами.