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

После вызова C функции Rust хочет передать буфер в Vec::from raw parts. Какой контракт владения и размещени...

После вызова C-функции Rust хочет передать буфер в Vec::from_raw_parts. Какой контракт владения и размещения должен быть подтверждён до этого вызова?

unsafe extern "C" {
    fn c_alloc(size: usize) -> *mut u8;
}

fn receive(n: usize) -> Vec<u8> {
    let p = unsafe { c_alloc(n) };
    unsafe { Vec::from_raw_parts(p, n, n) }
}
Проходите собеседования с ИИ помощником Hintsage

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

Vec::from_raw_parts допустим только тогда, когда указатель указывает на память, которую Rust вправе освободить своим аллокатором, а len, cap, выравнивание и размер точно соответствуют выделению. Первые len элементов должны быть полностью инициализированы. Сам факт, что C выделил достаточно байт, этого контракта не выполняет.

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

Vec::from_raw_parts нужен для передачи владения буфером без копирования: указатель, длина и вместимость снова объединяются в управляемый Rust-контейнер. Такой механизм особенно полезен при обмене данными между низкоуровневым Rust-кодом и FFI.

Однако Vec не является нейтральным представлением произвольной C-памяти. При уничтожении он освобождает буфер по правилам Rust, поэтому восстановление Vec одновременно восстанавливает и обязанность корректно освободить память.

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

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

Если C использует malloc, а Rust затем уничтожит Vec, возможен вызов несовместимого освобождения. Если n превышает фактически инициализированное количество элементов, Rust прочитает неинициализированную память; если cap неверен, при последующем росте или освобождении будет использована неправильная раскладка.

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

Перед вызовом нужно доказать все следующие условия:

  • p указывает на выделенный блок, совместимый с освобождением через аллокатор Rust;
  • p не является нулевым и корректно выровнен для u8;
  • len <= cap;
  • первые len элементов типа u8 инициализированы;
  • фактическая раскладка блока соответствует cap * size_of::<u8>() и нужному выравниванию;
  • размер выделения не нарушает ограничения Vec, включая ограничение на общий размер объекта;
  • Rust получает единственное владение блоком и никто больше не освободит его или не будет использовать после передачи.

После успешного вызова Vec::from_raw_parts отвечает за память. Нельзя затем передавать тот же указатель в free, вызывать C-деструктор или повторно создавать из него другой владеющий контейнер.

Для обычного C-буфера безопаснее скопировать данные в Rust: временно проверить указатель, длину и инициализированность, создать срез через from_raw_parts, затем вызвать .to_vec(). В этом случае C сохраняет владение исходным блоком, а Rust владеет независимой копией.

Если копирование недопустимо, API должен явно согласовать аллокатор и владельца. Например, Rust может передать C указатель, полученный из Vec, а C обязан вернуть его в специальную Rust-функцию освобождения с исходными capacity и типом. Параметр len при освобождении не должен использоваться как признак числа инициализированных элементов; важны исходные capacity и корректная раскладка выделения.

Для нулевой длины нельзя безоговорочно передавать нулевой указатель: требования Vec к указателю остаются значимыми. Нулевой результат C нужно обработать отдельно, а для пустого Vec использовать обычный Vec::new() либо корректный ненулевой dangling-указатель, предусмотренный Rust-абстракцией.

Минимальная схема передачи Rust-буфера без копирования выглядит так:

use std::mem::forget; fn expose(mut v: Vec<u8>) -> (*mut u8, usize, usize) { let result = (v.as_mut_ptr(), v.len(), v.capacity()); forget(v); result }

Такой указатель можно передать C только вместе с контрактом: C не освобождает его самостоятельно, не выходит за capacity и возвращает владение функции Rust, которая восстановит Vec с теми же параметрами.

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

C-библиотека возвращает массив, выделенный через собственный пул памяти, и предоставляет функцию c_free. Вариант с Vec::from_raw_parts выглядит эффективным, но неверен: Vec при уничтожении не вызовет c_free, поэтому даже совпадение размера и выравнивания не устраняет несовместимость владения.

Можно скопировать данные в Vec: это проще всего доказать и не связывает время жизни Rust-контейнера с C-пулом, но требует O(n) времени и дополнительной памяти. Можно оставить указатель в обёртке с ручным вызовом c_free: копирования нет, но приходится самостоятельно поддерживать длину, время жизни и потокобезопасность.

Практичный выбор — обёртка, которая хранит указатель, длину и принадлежность C-пулу, а в Drop вызывает именно c_free. Если буфер нужен после освобождения C-ресурса или должен изменяться как обычный Vec, выполняется явная копия. Это разделяет две модели владения вместо опасной попытки выдать C-память за Rust-выделение.

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

  1. Достаточно ли передать len == cap, если C выделил ровно нужное число байт?

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

  2. Можно ли указать len = 0, чтобы обойти сомнение в инициализации C-буфера?

    Это снимает требование об инициализации элементов, но не делает память автоматически пригодной для Vec. Указатель, вместимость, аллокатор и раскладка всё равно должны быть корректными. Кроме того, при последующем set_len Rust будет считать новые элементы инициализированными, поэтому их нужно предварительно фактически записать.

  3. Почему нельзя восстановить C-буфер сначала в Vec<u8>, а затем передать его обратно в C для освобождения?

    Потому что после from_raw_parts владельцем становится Vec, а C уже не должен самостоятельно освобождать тот же блок. Передача указателя в c_free создаёт двойное освобождение или использование несовместимого аллокатора. Если освобождать должен C, буфер нужно оставить под контролем C и описать это в отдельной обёртке без восстановления владеющего Vec.