Внешняя библиотека сохраняет переданный ей указатель после возврата функции. Какой инвариант должен обеспечить Rust-код, чтобы последующее обращение по нему оставалось корректным?
Rust-код должен гарантировать, что память по указателю остаётся выделенной, доступной и неизменной по необходимому контракту до момента, когда внешняя библиотека окончательно перестанет его использовать. Нельзя передавать указатель на локальное значение или освобождать объект сразу после вызова: иначе библиотека получит висячий указатель, а последующее обращение приведёт к неопределённому поведению.
Модель владения и заимствований Rust проверяет сроки жизни указателей внутри Rust-кода. На границе FFI компилятор не видит, сохраняет ли C-библиотека адрес, вызывает ли его позже и в каком потоке это происходит.
Поэтому длительность действия указателя, переданного во внешний код, становится частью явного контракта FFI. Подход с явным управлением временем жизни решает исходную проблему C-интерфейсов: указатель сам по себе не содержит информации о том, кто и когда может его использовать.
Указатель на локальную переменную становится недействительным после выхода из функции. Аналогично, указатель на объект в куче становится висячим после освобождения объекта, даже если его числовой адрес пока не занят другой памятью.
Ошибки возникают не только при немедленном использовании указателя. C-библиотека может сохранить его для асинхронного callback, очереди событий или фонового потока и обратиться к нему уже после возврата из Rust-функции. Дополнительно нужно учитывать, может ли библиотека читать память, изменять её или обращаться к ней одновременно с Rust-кодом.
Сначала нужно определить контракт: указатель является временным заимствованием или передаётся вместе с владением. При временном заимствовании Rust обязан удерживать объект живым до документированного завершения всех обращений C; если библиотека сохраняет адрес, простой срок вызова функции недостаточен.
При передаче владения объект обычно размещают в куче и передают стабильный адрес. Освобождение выполняется только после подтверждённого события, означающего, что C больше не использует указатель. Если память выделил Rust, освобождать её должен совместимый с Rust механизм, а не произвольный вызов free; при передаче владения этот момент должен быть однозначно закреплён контрактом.
Минимальная схема передачи владения выглядит так:
Box::into_raw не освобождает объект и возвращает указатель на него. Box::from_raw допустимо вызывать ровно один раз для указателя, полученного из соответствующего Box::into_raw, и только после того, как внешний код прекратил обращения. Повторное восстановление Box, раннее освобождение или использование указателя после release_context нарушают инварианты владения.
Стабильный адрес сам по себе не решает проблему. Нужно также обеспечить корректную инициализацию, допустимое выравнивание, отсутствие гонок, совместимое представление типа и соблюдение правил доступа к памяти. Если C вызывает callback из другого потока, срок жизни и синхронизация должны быть рассчитаны на такую модель выполнения.
Асинхронная C-библиотека принимает указатель на контекст и вызывает callback позже. Передача адреса локальной структуры привела бы к обращению к уже уничтоженному объекту; выделение структуры в куче без механизма освобождения создало бы утечку.
Рассматриваемые варианты:
Box::into_raw с освобождением при unregister — требует строгого протокола, зато явно связывает срок жизни с использованием.Выбирается третий вариант: регистрация возвращает дескриптор, операция отмены сначала синхронно дожидается завершения callback, затем восстанавливает Box и освобождает контекст. Результат — указатель остаётся действительным весь период возможного использования, а точка освобождения определяется протоколом, а не случайным временем выхода из области видимости.
Нет. Размещение в куче обычно обеспечивает стабильность адреса до освобождения, но не определяет, когда внешний код закончит работу. Нужны явные границы: событие отмены, завершение операции или другой механизм, после которого указатель больше не используется.
Только если контракт гарантирует, что библиотека не сохраняет указатель и не использует его после возврата. Сам факт успешного вызова регистрации ничего не доказывает: библиотека могла принять адрес для последующего callback. При неизвестном контракте безопасный Rust-интерфейс должен либо запретить такую передачу, либо удерживать объект до явного подтверждения завершения.
Нет. Живой объект может одновременно изменяться Rust-кодом и C-кодом без необходимой синхронизации, что создаёт гонку данных и неопределённое поведение. Помимо срока жизни нужно установить правила доступа: например, передавать неизменяемое состояние, защищать изменяемое состояние синхронизацией или гарантировать, что callback и операции Rust не выполняются одновременно.