Представьте вызов C-функции, которая записывает результат по переданному указателю. Как Rust должен представить такой выходной параметр до вызова, чтобы не считать память инициализированной преждевременно?
Для выходного параметра следует использовать MaybeUninit<T>, передав C указатель, полученный через as_mut_ptr(). После вызова значение можно прочитать только при доказанном условии, что C-функция полностью и корректно записала объект типа T; тогда применяется assume_init().
Нельзя заранее создавать обычную переменную T «для записи поверх неё»: это утверждает, что значение уже инициализировано, хотя C ещё ничего не записал.
При взаимодействии с C распространён шаблон out-параметра: вызывающая сторона выделяет место, а вызываемая функция заполняет его. В Rust обычная переменная имеет строгий инвариант инициализации, поэтому нельзя безопасно представить такое место как уже существующее значение T.
MaybeUninit<T> отделяет хранилище нужного размера и выравнивания от самого инициализированного значения. Это позволяет явно перенести ответственность за момент и корректность инициализации на код, который взаимодействует с внешней функцией.
Если передать C указатель на неинициализированную память, используя обычный &mut T, Rust-код ложно сообщает компилятору, что корректный T уже существует. Даже если ссылка немедленно преобразуется в raw pointer, исходное создание ссылки могло нарушить инвариант типа.
Обратная ошибка также опасна: вызов assume_init() до полной записи превращает неинициализированные байты в значение T. Это неопределённое поведение; для типов с нетривиальным уничтожением последующий drop может дополнительно обратиться с некорректным значением.
MaybeUninit<T> предоставляет неинициализированное хранилище, но не считается значением T. Его as_mut_ptr() можно передать внешней функции как *mut T, не создавая ссылку на ещё несуществующий объект.
В этом примере безопасность assume_init() основана не на самом типе bool, а на контракте C-функции: при true она должна записать полностью корректный u32 в out, не выходя за границы и не сохраняя указатель после завершения вызова. Этот контракт должен быть проверен разработчиком или документирован внешним API.
Если функция может записать результат частично, Rust не должен считать T инициализированным. Для структур нужно также обеспечить совместимое представление, обычно через repr(C), а все поля должны быть корректно инициализированы согласно их собственным инвариантам.
MaybeUninit не решает вопросы владения, времени жизни, потоковой безопасности или корректности ABI. Он решает только представление хранилища до инициализации; остальные гарантии остаются частью FFI-контракта.
Библиотека C возвращает статус операции и принимает указатель на структуру результата. При ошибке она ничего не записывает, а при успехе полностью заполняет структуру.
Вариант с заранее созданной структурой Rust прост, но некорректен: структура должна получить допустимые начальные значения, а C фактически требует лишь неинициализированное место. Вариант с ручным массивом байтов хуже контролируется: легко ошибиться с выравниванием, размером или последующим преобразованием.
Выбранное решение — MaybeUninit<ResultStruct> с repr(C) у структуры. Указатель передаётся только на время вызова, а assume_init() выполняется исключительно после успешного статуса. Такой подход сохраняет инвариант инициализации Rust, явно фиксирует границу доверия к C и не требует лишней предварительной инициализации.
Достаточно ли проверить только код успешного возврата C-функции?
Нет. Нужно знать точный контракт: означает ли успешный статус полную инициализацию всех байтов и полей, допускает ли функция нулевой указатель, может ли она записать только часть результата. Если контракт не гарантирует полной инициализации, assume_init() применять нельзя.
Можно ли использовать MaybeUninit<T> для типа с деструктором, если C заполняет его побайтно?
Не автоматически. Произвольная побайтовая запись не гарантирует создание корректного значения Rust-типа: у него могут быть ограничения на допустимые значения, владение и внутренние ссылки. Такой способ допустим только при тщательно определённом представлении и контракте; для обычных Rust-типов безопаснее использовать FFI-совместимые структуры из простых полей.
Что произойдёт, если C сохранит переданный указатель после возврата?
Указатель на локальный MaybeUninit станет недействительным после выхода функции, поэтому последующая запись через него приведёт к неопределённому поведению. Если C должен хранить указатель, хранилище обязано иметь подходящее время жизни и правила владения, например быть размещённым в памяти, которая живёт дольше вызова и освобождается согласованным способом.