При передаче структуры Rust в C какое представление нужно выбрать, чтобы C корректно использовал смещения её полей?
Нужно применить к структуре #[repr(C)]. Оно задаёт порядок полей, выравнивание и размещение полей по правилам, совместимым с C, поэтому C сможет использовать ожидаемые смещения.
Однако #[repr(C)] решает только задачу представления структуры. Оно не делает автоматически безопасными вложенные Rust-типы, указатели, владение памятью или значения, передаваемые через FFI.
Обычное представление структуры Rust не обязано сохранять порядок полей, вставлять поля в память по правилам C или оставаться неизменным между версиями компилятора. Компилятор может выбирать расположение, выгодное для оптимизации размера и выравнивания.
C ABI, напротив, требует согласованного физического представления данных: сторона C должна знать порядок полей, их размеры, выравнивание и смещения. Атрибут repr(C) предназначен для явного выбора такого соглашения на границе языков.
Если передать в C структуру с обычным представлением Rust, C-компилятор может считать, что второе поле находится по одному смещению, тогда как Rust разместил его иначе. Чтение или запись по неверному смещению приводит к повреждению данных, неопределённому поведению или трудно диагностируемым ошибкам.
Даже при #[repr(C)] остаются другие условия безопасности: типы полей должны иметь согласованный смысл в обоих языках, указатели должны быть действительными, а время жизни и владение памятью — явно определёнными. Например, String, Vec<T> и ссылки Rust нельзя считать автоматически совместимыми с аналогичными на вид структурами C.
#[repr(C)] заставляет Rust раскладывать поля структуры по правилам C для соответствующих типов. Это включает порядок объявления полей, выравнивание и вставку padding между полями и в конце структуры.
В этом примере C может использовать структуру с двумя совместимыми 32-битными целыми числами, если его объявление имеет такое же соглашение. extern "C" отдельно задаёт ABI функции; #[repr(C)] задаёт представление самой структуры. Одно не заменяет другое.
Надёжный FFI-тип обычно строят из типов с явно согласованной семантикой: целых фиксированной ширины, указателей на заранее оговорённые данные и других типов, совместимость которых подтверждена контрактом интерфейса. Вложенные структуры также должны иметь совместимое представление, если их содержимое читается другой стороной.
#[repr(C)] не гарантирует, что размер и значение Rust-типа совпадают с ожиданиями C во всех случаях. Особенно осторожно нужно обращаться с bool, перечислениями, указателями на Rust-объекты, ссылками, trait object и типами, содержащими управление ресурсами.
Компромисс заключается в том, что явное C-представление ограничивает свободу оптимизаций layout-компилятора, зато делает бинарный контракт проверяемым и устойчивым для FFI. Обычно это оправданная цена на границе языков; внутри чистого Rust следует по возможности оставлять стандартное представление.
Команда передаёт из Rust в C описание сетевого пакета с полями идентификатора и длины. При тестах на одной сборке всё работает, но после изменения полей C начинает читать неверную длину.
Первый вариант — оставить обычное представление Rust. Его плюс — отсутствие дополнительных аннотаций и свобода оптимизации, но расположение полей не является подходящим контрактом для C.
Второй вариант — добавить #[repr(C)], но сохранить в структуре String и ссылку Rust. Смещения будут согласованы, однако C всё равно не сможет корректно интерпретировать внутреннее представление и правила владения этими полями.
Выбран третий вариант: использовать #[repr(C)], заменить поля на типы с явно согласованным C-представлением и передавать буфер как указатель с длиной по отдельному контракту. В результате смещения стали стабильными, а границы владения и допустимые операции были определены явно; проблема исчезла не только для текущей сборки, но и для последующих изменений структуры.
#[repr(C)] для совместимости вложенной структуры?Нет. Если поле имеет тип другой структуры, её собственное представление тоже должно быть совместимо с C, когда C обращается к её полям. #[repr(C)] внешнего типа не превращает вложенный Rust-тип с обычным представлением в C-совместимый.
Практически это означает, что все составные типы на FFI-границе нужно проверять рекурсивно. Нельзя делать вывод о безопасности только по атрибуту внешней структуры.
#[repr(C)] одинаковый размер структуры в Rust и C?Он задаёт правила размещения, но одинаковый размер получается только при совпадении типов, ABI, выравнивания и настроек целевой платформы. Например, Rust-тип и C-тип могут иметь похожее назначение, но различаться размером или допустимыми значениями.
Поэтому контракт должен фиксировать конкретные типы и платформенные предположения. Для критичных интерфейсов размеры и выравнивание проверяют средствами сборки или тестами на обеих сторонах, а не полагаются лишь на визуальное сходство объявлений.
Vec<T>?Нет, не автоматически. Ссылка Rust несёт инварианты валидности, выравнивания и времени жизни, а Vec<T> содержит внутренние детали управления буфером; C не обязан соблюдать эти инварианты и не знает правил освобождения.
Вместо этого обычно передают указатель и длину с явно описанными условиями, а операции чтения, записи и освобождения оставляют у стороны, которой принадлежит ресурс. Если C должен хранить объект Rust, ему передают непрозрачный указатель и предоставляют функции FFI для разрешённых операций, не раскрывая внутреннее представление объекта.