Программирование RustUnsafe и памятьИнженер по системному программированию на Rust

Сценарий: FFI возвращает указатель на буфер и его длину. Что нужно доказать перед созданием из них Rust среза?

Сценарий: FFI возвращает указатель на буфер и его длину. Что нужно доказать перед созданием из них Rust-среза?

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

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

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

При FFI эти свойства обычно не проверяются автоматически: их гарантирует контракт внешней функции и безопасная обёртка. Если хотя бы одно условие нарушено, создание или последующее использование среза может привести к неопределённому поведению.

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

Raw pointers нужны Rust для взаимодействия с системными API, аппаратными ресурсами и кодом на C-подобных языках. Такие указатели не несут гарантий владения, времени жизни, уникальности доступа или корректности диапазона.

Срез &[T] удобнее raw pointer: он содержит указатель и длину, а код Rust может использовать его через безопасные операции. Однако преобразование raw pointer в ссылку переносит ответственность за соблюдение инвариантов с компилятора на программиста.

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

FFI-буфер может быть освобождён внешним кодом сразу после возврата, иметь меньший фактический размер, содержать неинициализированные данные или пересекать границу нескольких аллокаций. Числовая пара «указатель плюс длина» сама по себе не доказывает ни одного из этих свойств.

Неверное преобразование может привести к чтению за границами, использованию памяти после освобождения, нарушению выравнивания или созданию ссылки на неинициализированное значение. В Rust это не просто ошибка результата: такое поведение нарушает модель памяти и делает поведение всей программы неопределённым.

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

Для создания &[T] из указателя и длины должны выполняться следующие условия:

  • указатель не должен быть нулевым и должен иметь выравнивание для T, включая случай нулевой длины;
  • диапазон должен содержать len корректно инициализированных значений T;
  • весь диапазон должен находиться в одной аллокации и быть доступным для чтения;
  • размер диапазона не должен превышать ограничения, совместимые с isize;
  • память должна оставаться доступной и неизменной несовместимым образом в течение времени жизни созданного среза.

Для &mut [T] добавляется более сильное требование: на соответствующий диапазон не должно существовать других активных ссылок или конкурирующего доступа. Это особенно важно, когда иностранный код может одновременно изменять тот же буфер.

Создание среза не копирует данные и не продлевает их время жизни. Если FFI-буфер принадлежит внешней библиотеке, безопасная обёртка должна либо удерживать ресурс живым на весь срок использования среза, либо немедленно скопировать данные во владение Rust.

Например, обёртка ниже ограничивает срок использования внешней памяти временем копирования. Её контракт всё равно требует, чтобы внешний код предоставил доступный диапазон байтов; Rust не может проверить это по одному raw pointer.

use std::slice; /// Safety: ptr указывает на len инициализированных байтов, /// доступных для чтения во время вызова. unsafe fn copy_foreign(ptr: *const u8, len: usize) -> Vec<u8> { if len == 0 { return Vec::new(); } assert!(!ptr.is_null()); assert!(len <= isize::MAX as usize); unsafe { slice::from_raw_parts(ptr, len) }.to_vec() }

Проверка len и ненулевого указателя не доказывает принадлежность диапазона одной аллокации или его фактическую доступность. Поэтому такие проверки дополняют, но не заменяют договорённость с FFI и анализ жизненного цикла буфера.

Основной компромисс таков: заимствованный срез эффективен и не требует копирования, но связывает Rust-код с временем жизни и правилами доступа внешнего буфера. Копирование дороже по времени и памяти, зато обычно позволяет вернуть безопасный владеющий тип и разорвать эту зависимость.

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

Внешняя библиотека возвращает указатель на сетевой пакет и длину. Буфер принадлежит внутреннему пулу библиотеки и может быть переиспользован при следующем вызове обработки пакета.

Рассматривались два варианта. Первый — вернуть из Rust заимствованный срез: это исключает копирование, но требует гарантировать, что пул не переиспользует буфер и не освобождает его до окончания времени жизни среза. Второй — сразу скопировать байты в Vec<u8>: это добавляет копирование, зато результат больше не зависит от жизненного цикла FFI-буфера.

Выбран второй вариант, поскольку контракт библиотеки не гарантировал стабильность памяти после возврата из callback. В результате Rust-код получил собственное владение данными, а риск использования освобождённого или переиспользованного буфера остался внутри короткой проверяемой unsafe-обёртки.

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

  1. Достаточно ли ненулевого указателя и корректной длины для создания среза?

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

  1. Продлевает ли созданный срез жизнь FFI-буфера?

Нет. Срез является заимствованием и не управляет ресурсом, на который указывает. Если внешняя библиотека освободит или переиспользует буфер, срез станет недействительным, даже если его тип и время жизни формально позволяют продолжить использование.

  1. Можно ли безопасно вернуть срез из callback, если внешний код обещает не менять буфер?

Только если доказан весь необходимый контракт: буфер остаётся живым после callback, диапазон полностью доступен, данные инициализированы, а условия доступа действительно сохраняются весь срок жизни среза. Одного обещания «не менять» недостаточно, если библиотека может освободить буфер, изменить его размер или передать указатель на временную память; при сомнительном времени жизни следует копировать данные.