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

Как unsafe extern определяет место проверки контракта безопасности при вызове FFI функции?

Как unsafe extern определяет место проверки контракта безопасности при вызове FFI-функции?

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

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

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

Такая классификация не проверяет C-код автоматически. Если функцию ошибочно объявить как safe, хотя ей нужны предусловия, это может сделать безопасный Rust-код неопределённо некорректным.

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

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

Подход с unsafe extern делает границу доверия явной. В современных версиях Rust, включая редакцию 2024, сам блок внешних объявлений помечается как unsafe, а безопасность отдельных функций фиксируется непосредственно в их объявлениях.

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

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

Если объявить такую функцию safe, любой безопасный Rust-код сможет вызвать её без проверки. Ошибка в объявлении перенесёт потенциальное неопределённое поведение из явно обозначенной unsafe-области в обычный безопасный код и нарушит модель безопасности Rust.

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

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

Функция, объявленная как unsafe, может вызываться только внутри unsafe-контекста. Вызывающий обязан проверить все предусловия: валидность указателей, размеры и время жизни буферов, требования к потокам, состояние глобального C-кода и допустимость повторного входа.

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

unsafe extern "C" { pub safe fn c_version() -> u32; pub unsafe fn c_sum(ptr: *const u8, len: usize) -> i32; } fn version() -> u32 { c_version() } fn sum(bytes: &[u8]) -> i32 { unsafe { c_sum(bytes.as_ptr(), bytes.len()) } }

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

Обычно небезопасную FFI-функцию оставляют unsafe, если её контракт невозможно полностью выразить типами Rust. Безопасную обёртку делают safe только после проверки всех условий внутри неё; это уменьшает число unsafe-участков, но переносит ответственность на разработчика обёртки.

Важно различать безопасность вызова и корректность ABI. unsafe extern не исправляет несовпадение типов, соглашения о вызовах, представления структур или правила освобождения памяти. Эти свойства должны быть согласованы отдельно.

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

Команда подключает C-библиотеку с функцией поиска, принимающей указатель на массив байтов и длину. Один вариант — объявить её safe, чтобы упростить API. Плюс этого решения — удобные вызовы, минус — любой код сможет передать некорректную пару указатель-длина, а ошибка объявления сделает небезопасным весь безопасный интерфейс.

Второй вариант — оставить функцию unsafe и требовать ручной проверки при каждом вызове. Это честно отражает контракт, но дублирует проверки и повышает риск ошибки в разных местах.

Выбранное решение — объявить внешнюю функцию unsafe, а поверх неё создать одну безопасную обёртку, принимающую срез Rust и проверяющую дополнительные ограничения библиотеки. В результате проверка сосредоточена в одном месте, вызовы приложения остаются безопасными, а граница доверия явно ограничена FFI-слоем.

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

  1. Достаточно ли поместить объявление в unsafe extern, чтобы вызов стал безопасным?

Нет. unsafe extern описывает доверенный блок объявлений, но безопасность вызова определяется классификацией конкретной функции. Для unsafe fn вызывающий по-прежнему обязан использовать unsafe и выполнить предусловия.

  1. Можно ли объявить FFI-функцию safe, если она принимает raw pointer?

Сам факт наличия raw pointer не запрещает safe-объявление, но делает отсутствие предусловий маловероятным. Оно допустимо только при доказуемом контракте: например, функция может трактовать null как допустимое значение и не читать память. Если же требуется валидный объект, правильнее оставить функцию unsafe или скрыть указатель за безопасной обёрткой.

  1. Что проверяет компилятор после объявления safe FFI-функции?

Он проверяет синтаксис, типы, доступность символа на этапе линковки и правила вызова согласно указанному ABI, но не проверяет реализацию функции и её фактические предусловия. Компилятор не доказывает корректность указателей, владение памятью, потоковые ограничения или отсутствие побочных нарушений контракта C. Поэтому safe в объявлении является утверждением разработчика, а не автоматически выведенной гарантией.