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

Внешняя библиотека сохраняет переданный ей указатель после возврата функции. Какой инвариант должен обеспеч...

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

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

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

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

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

Модель владения и заимствований Rust проверяет сроки жизни указателей внутри Rust-кода. На границе FFI компилятор не видит, сохраняет ли C-библиотека адрес, вызывает ли его позже и в каком потоке это происходит.

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

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

Указатель на локальную переменную становится недействительным после выхода из функции. Аналогично, указатель на объект в куче становится висячим после освобождения объекта, даже если его числовой адрес пока не занят другой памятью.

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

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

Сначала нужно определить контракт: указатель является временным заимствованием или передаётся вместе с владением. При временном заимствовании Rust обязан удерживать объект живым до документированного завершения всех обращений C; если библиотека сохраняет адрес, простой срок вызова функции недостаточен.

При передаче владения объект обычно размещают в куче и передают стабильный адрес. Освобождение выполняется только после подтверждённого события, означающего, что C больше не использует указатель. Если память выделил Rust, освобождать её должен совместимый с Rust механизм, а не произвольный вызов free; при передаче владения этот момент должен быть однозначно закреплён контрактом.

Минимальная схема передачи владения выглядит так:

struct Context { value: u32, } fn allocate_context() -> *mut Context { Box::into_raw(Box::new(Context { value: 42 })) } unsafe fn release_context(ptr: *mut Context) { drop(Box::from_raw(ptr)); }

Box::into_raw не освобождает объект и возвращает указатель на него. Box::from_raw допустимо вызывать ровно один раз для указателя, полученного из соответствующего Box::into_raw, и только после того, как внешний код прекратил обращения. Повторное восстановление Box, раннее освобождение или использование указателя после release_context нарушают инварианты владения.

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

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

Асинхронная C-библиотека принимает указатель на контекст и вызывает callback позже. Передача адреса локальной структуры привела бы к обращению к уже уничтоженному объекту; выделение структуры в куче без механизма освобождения создало бы утечку.

Рассматриваемые варианты:

  • Локальная переменная — проста, но недопустима после возврата из функции.
  • Указатель на объект в куче без явного завершения — сохраняет память живой, но создаёт утечку.
  • Передача владения через Box::into_raw с освобождением при unregister — требует строгого протокола, зато явно связывает срок жизни с использованием.
  • Счётчик ссылок — удобен при нескольких владельцах, но требует корректно удерживать и затем ровно один раз восстанавливать соответствующий объект, а также синхронизировать доступ.

Выбирается третий вариант: регистрация возвращает дескриптор, операция отмены сначала синхронно дожидается завершения callback, затем восстанавливает Box и освобождает контекст. Результат — указатель остаётся действительным весь период возможного использования, а точка освобождения определяется протоколом, а не случайным временем выхода из области видимости.

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

  1. Достаточно ли того, что объект находится в куче?

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

  1. Можно ли освободить объект сразу после возврата из функции регистрации?

Только если контракт гарантирует, что библиотека не сохраняет указатель и не использует его после возврата. Сам факт успешного вызова регистрации ничего не доказывает: библиотека могла принять адрес для последующего callback. При неизвестном контракте безопасный Rust-интерфейс должен либо запретить такую передачу, либо удерживать объект до явного подтверждения завершения.

  1. Решает ли продление времени жизни проблему при параллельном доступе?

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