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

В каких условиях новая Rust обёртка над C типом может сохранить ABI исходного значения при передаче через FFI?

В каких условиях новая Rust-обёртка над C-типом может сохранить ABI исходного значения при передаче через FFI?

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

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

Для сохранения ABI обёртка обычно должна быть структурой с атрибутом #[repr(transparent)] и ровно одним полем ненулевого размера, например указателем на непрозрачный C-тип. Тогда её представление и ABI гарантированно совпадают с представлением этого единственного поля.

Одного совпадения размера или выравнивания недостаточно. repr(transparent) решает только задачу представления; корректность указателя, владение, время жизни и допустимость вызова всё равно должны обеспечиваться отдельно.

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

По умолчанию Rust не обязан сохранять конкретный порядок полей, размер или ABI пользовательских структур. Это позволяет компилятору оптимизировать представление типов, но делает такую структуру непригодной для прямой передачи в C-код.

FFI требует явного контракта между двумя компиляторами. Атрибуты repr(C) и repr(transparent) появились как способы выразить этот контракт: первый задаёт C-подобное представление составных структур, второй — прозрачность обёртки над одним значимым полем.

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

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

Ошибочное представление может привести к передаче аргумента в неверном регистре или области стека, неправильному чтению C-функцией и неопределённому поведению. Даже при корректном ABI вызов остаётся небезопасным, если указатель нулевой, висячий, недействителен для записи или используется после освобождения объекта.

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

Минимальная обёртка над одним указателем может выглядеть так:

#[repr(transparent)] struct CHandle(*mut CHandleRaw); #[repr(C)] struct CHandleRaw { _private: [u8; 0], } unsafe extern "C" { fn c_close(handle: CHandle); } fn close(handle: CHandle) { unsafe { c_close(handle) } }

repr(transparent) гарантирует, что CHandle имеет то же представление и ABI, что и его единственное поле *mut CHandleRaw. Поэтому декларация FFI может использовать обёртку вместо непосредственного указателя, если это соответствует C-прототипу.

У обёртки должен быть только один ненулевой по размеру значимый компонент. Дополнительные поля нулевого размера не должны менять это правило, но добавление второго обычного поля уже превращает тип в составную структуру, для которой repr(transparent) неприменим.

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

Важно отличать ABI от безопасности. Атрибут не проверяет, что указатель указывает на объект нужного типа, что объект ещё жив, что C имеет право его изменять и что вызов разрешён текущим потоком. Эти свойства должны быть закреплены инвариантами обёртки и проверены в её unsafe-реализации.

Приватное поле полезно для контроля инвариантов: безопасный код не сможет произвольно создать значение обёртки, а конструктор сможет проверить, например, что указатель не является нулевым. Однако проверка на ненулевой адрес сама по себе не доказывает действительность объекта и правильность его времени жизни.

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

Команда оборачивает дескриптор, возвращаемый C-библиотекой. Первый вариант — передавать по всему Rust-коду *mut CHandleRaw. Его преимущество — простота и очевидное соответствие C-прототипу. Недостатки — отсутствие типовой защиты, возможность случайно смешать разные виды дескрипторов и сложность централизовать освобождение.

Второй вариант — объявить обёртку без repr. Это улучшает типобезопасность на уровне Rust, но не даёт гарантии совместимого ABI. Такой вариант нельзя считать достаточным только потому, что обёртка сейчас имеет размер указателя.

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

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

  1. Достаточно ли repr(transparent), если обёртка содержит нулевой указатель?

Нет. Атрибут гарантирует только представление и ABI. Нулевой указатель может быть допустимым специальным значением для конкретной C-функции, но если функция ожидает действительный дескриптор, вызов нарушит её контракт.

Решение о допустимости нулевого значения принимает API библиотеки. Rust-обёртка должна либо запрещать нулевой указатель при создании, либо явно моделировать его как отдельное состояние, например через Option или специальный тип результата.

  1. Можно ли передать прозрачную обёртку в C, если её поле имеет Rust-специфичный тип?

Совпадение ABI обёртки с полем не означает, что сам тип поля корректен для C. Например, Rust-ссылка, bool, функция с несовместимым ABI или тип с недокументированным представлением могут нарушать контракт независимо от repr(transparent).

Поле должно иметь представление и семантику, которые ожидает C: обычно это сырой указатель, целое число фиксированного размера или функция с явно заданным extern "C" ABI. Кроме того, должны соблюдаться правила валидности этого значения на стороне C.

  1. Нужно ли использовать repr(C) вместо repr(transparent) для обёртки над указателем?

repr(C) может задать C-подобное представление структуры, но он не выражает именно требование эквивалентности ABI единственного поля. Для newtype-обёртки над одним значимым полем правильнее использовать repr(transparent), поскольку он непосредственно гарантирует прозрачность относительно этого поля.

repr(C) нужен для структур и объединений, чьё внутреннее расположение должно соответствовать C. Выбор атрибута зависит от формы контракта: прозрачная оболочка над одним значением — repr(transparent), составной объект с C-полями — repr(C).