Что обязан доказать Rust-код перед вызовом указателя на функцию, полученного из FFI?
Rust-код должен доказать, что указатель обозначает существующую вызываемую функцию, не является недопустимым значением, имеет точно совместимые сигнатуру и ABI, а сам вызов соответствует контракту этой функции. Проверка компилятора этого не устанавливает: после перехода через FFI ответственность за инварианты лежит на unsafe-коде и его безопасной обёртке.
Указатели на функции широко применяются в C для callback-механизмов, таблиц операций, плагинов и динамически загружаемых библиотек. Такие интерфейсы появились в среде, где компилятор не мог выразить владение, время жизни и точную безопасность вызова в типовой системе.
Rust сохраняет возможность взаимодействия с этим кодом, но не может автоматически доказать, что адрес действительно указывает на функцию нужного ABI или что внешняя библиотека не вернула устаревший адрес. Поэтому FFI разделяет проверяемую компилятором часть контракта и предположения, которые обязан подтвердить программист.
Числовое значение, похожее на адрес, само по себе не доказывает, что по нему находится функция. Указатель может быть нулевым, повреждённым, уже недействительным или относиться к функции с другой сигнатурой и соглашением о вызовах.
Неверный вызов может привести к неопределённому поведению: повреждению регистров или стека, неправильной интерпретации аргументов, нарушению выравнивания данных и последующему падению далеко от места ошибки. Даже если один вызов случайно работает, это не делает нарушенный контракт безопасным.
Перед вызовом нужно установить следующие факты:
Особенно важен ABI. Совпадение Rust-сигнатуры недостаточно: внешняя функция обычно должна быть описана с ABI, который обещан библиотекой, например extern "C". ABI определяет соглашение о передаче аргументов, возвращении значения и взаимодействии с машинным кодом.
Надёжнее получать из FFI типизированный указатель на функцию, а не восстанавливать его из целого числа или универсального указателя. Преобразование адреса в указатель на функцию не создаёт доказательств корректности и может быть непереносимым, если исходный FFI-контракт не гарантирует такое преобразование.
Минимальный пример проверки нулевого указателя и явного ABI:
Option<Callback> позволяет явно обработать нулевой указатель, если FFI-представление допускает его. Сам вызов остаётся unsafe, потому что тип не доказывает, что функция действительно существует, что она ещё загружена и что её внешний контракт соблюдён.
Безопасная обёртка может скрыть unsafe только после проверки всех условий, которые она способна проверить, и при наличии API-контракта для остальных. Если библиотека может вернуть произвольный адрес, выгрузить модуль во время использования callback или вызвать его из другого потока без синхронизации, обёртка должна либо запретить такие сценарии, либо выразить их требования в своём безопасном интерфейсе.
Плагин на C возвращает callback для обработки числа. Первый вариант — хранить адрес как целое число и при каждом вызове преобразовывать его в указатель на функцию. Плюс такого подхода — совместимость с низкоуровневым API; минусы — потеря типовой информации, отсутствие проверки ABI и риск использования адреса после выгрузки плагина.
Второй вариант — описать FFI-функцию как возвращающую типизированный Option<unsafe extern "C" fn(i32) -> i32>, проверить отсутствие None, а время жизни callback связать с объектом загруженного плагина. Это требует более строгого контракта, зато сохраняет ABI в типе и позволяет не вызывать функцию после освобождения её кода.
Выбирается второй вариант. На практике обёртка также не выгружает библиотеку, пока существуют зарегистрированные callback, и отдельно документирует, что вызывающий код обязан соблюдать требования внешней функции. В результате ошибка нулевого callback выявляется сразу, а риск вызова по адресу из выгруженного модуля устраняется управлением временем жизни.
Нет. Сигнатура описывает типы аргументов и результата на уровне Rust, но ABI определяет машинный способ вызова. Указатель на функцию с несовместимым ABI нельзя безопасно вызывать только потому, что визуально совпадают типы параметров.
Ненулевое значение может быть устаревшим адресом, адресом данных, функцией другой сигнатуры или кодом из уже выгруженной библиотеки. Проверка на ноль устраняет только один класс ошибок; остальные свойства должны следовать из FFI-контракта, типа указателя и управления временем жизни подключённого кода.
Нет. Тип подтверждает форму вызова, но не истинность внешних предположений. Функция может требовать конкретный диапазон аргументов, владение контекстом, вызов только из определённого потока или запрет параллельных вызовов. Безопасная обёртка должна доказать и эти условия либо сама оставаться unsafe.