Программирование RustUnsafe и памятьRust-разработчик системного ПО

Почему memcmp не подходит для проверки равенства значений repr C структуры Rust?

Почему memcmp не подходит для проверки равенства значений repr(C)-структуры Rust?

Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

memcmp сравнивает байтовое представление объекта, а не значения его полей. В repr(C)-структуре могут присутствовать padding-байты, содержимое которых не обязано быть одинаковым даже у структур с равными полями, поэтому побайтовое сравнение может дать ложное неравенство.

Кроме того, байтовое сравнение плохо подходит для полей вроде чисел с плавающей точкой, указателей и других типов с несколькими допустимыми представлениями. Для проверки значения нужно сравнивать поля по их семантике, а memcmp оставлять только для специально оговорённых протокольных представлений.

Исторический контекст

C активно использует операции над object representation — последовательностью байтов, занимаемой объектом в памяти. Это удобно для низкоуровневых протоколов, сериализации и системных API, но байтовое представление не всегда совпадает с логическим значением структуры.

Атрибут repr(C) появился для согласования порядка полей, выравнивания и размещения структуры с правилами C. Он делает layout предсказуемым для ABI, но не превращает структуру в безусловно сравнимый массив байтов и не устраняет padding.

Постановка проблемы

Рассмотрим структуру с полем размера один байт и последующим полем, требующим выравнивания в четыре байта:

#[repr(C)] struct Packet { flag: u8, value: u32, }

Между flag и value компилятор обычно размещает padding-байты. Они нужны для выравнивания value, но не являются частью значения какого-либо поля. При создании или изменении структуры эти байты могут иметь разные значения.

Если передать две такие структуры в memcmp, равные flag и value не гарантируют нулевой результат. Обратная передача структуры через FFI тоже требует учитывать её полный layout, включая padding: нельзя трактовать произвольный набор байтов как корректное значение Rust-типа без проверки его валидности.

Подробное решение

#[repr(C)] гарантирует совместимое с C размещение полей и правила выравнивания, но padding остаётся незначащей частью представления. Rust-код должен считать значимыми только сами поля и сравнивать их обычным PartialEq либо специализированной логикой.

memcmp допустим, когда контракт специально определяет равенство именно представлений. Например, это может быть фиксированный массив байтов протокола, где каждый байт и его значение заданы форматом. Для обычной структуры Rust такой контракт обычно отсутствует.

Есть и другие причины избегать memcmp для семантического сравнения:

  • два значения f32 могут иметь разные битовые представления и особые правила сравнения;
  • указатели с одинаковым смыслом могут иметь разные представления, а сравнение адресов не выражает равенство объектов;
  • padding может содержать остатки старых данных, что создаёт риск утечки информации при отправке структуры наружу;
  • изменение компилятора, архитектуры или ABI может изменить расположение и размер padding, не меняя значения полей.

Если C API требует сравнения или хеширования байтов, безопаснее явно преобразовать значения в определённый формат: сериализовать поля, обнулить или явно заполнить padding при необходимости и зафиксировать endianness. Само наличие repr(C) для этого недостаточно.

Ситуация из практики

Системный Rust-модуль передаёт в C repr(C)-структуру заголовка и хочет пропускать повторные заголовки по результату memcmp. При тестах на одной платформе это работает, но в production одинаковые заголовки иногда считаются различными.

Вариант с memcmp прост и быстр, но зависит от padding и конкретного способа инициализации памяти. Полное предварительное обнуление структуры уменьшает риск различий в padding, однако не решает проблемы семантики floating-point, указателей и переносимости такого контракта.

Выбранное решение — сравнивать поля заголовка явно, а для сетевого или дискового представления использовать отдельную сериализацию фиксированного формата. В результате сравнение стало независимым от padding, а формат обмена — стабильным между архитектурами.

Что кандидаты часто упускают

Дополнительный вопрос 1: Делает ли repr(C) padding-байты инициализированными?

Нет. repr(C) задаёт layout, но не обещает определённое содержимое padding. Инициализация полей не означает, что каждый байт между ними получил значение, поэтому полагаться на содержимое padding нельзя.

Дополнительный вопрос 2: Можно ли использовать memcmp, если структуру предварительно обнулить?

Иногда это делает конкретный ABI-контракт практически работоспособным, но само по себе не превращает сравнение в универсально корректное сравнение значений. Нужно отдельно доказать, что все поля имеют каноническое байтовое представление, padding всегда нормализован, а формат одинаков на всех целевых платформах.

Дополнительный вопрос 3: Опасно ли для Rust, если C прочитает padding через memcmp?

Чтение байтового диапазона объекта через C не означает чтение padding как значения Rust-типа, но результат всё равно нельзя интерпретировать как семантическое равенство структур. Практический риск — нестабильное сравнение и возможная утечка неинициализированных или ранее находившихся в памяти данных, если структура отправляется за пределы доверенного кода.