На границе C API optional callback представлен значением NULL. Как Rust должен описать такой параметр, чтобы отсутствие callback не превращалось в вызов недействительного адреса?
Используйте Option от указателя на функцию с правильным ABI: Option<extern "C" fn(...)>. Значение None соответствует нулевому указателю, а Some(callback) — действительному адресу функции; Rust не позволит случайно вызвать None как функцию.
Важно сохранить точную сигнатуру и extern "C". Option<fn(...)> с обычным Rust ABI не является корректной заменой callback, ожидаемому C-кодом.
C API традиционно обозначают необязательные callback-функции нулевым указателем. Это позволяет одной функции регистрации выразить оба состояния: callback подключён или callback отсутствует.
В Rust обычный указатель на функцию не имеет такого безопасного варианта отсутствия. Option решает задачу на уровне типа, а для указателей и указателей на функции использует нулевое значение как нишу, поэтому отдельный булев флаг обычно не требуется.
Если представить nullable callback как всегда действующий указатель на функцию, Rust-код может вызвать нулевой адрес. Это приводит не к обычной ошибке Rust, а к неопределённому поведению на границе FFI.
Другой риск — неверный ABI или сигнатура. Даже ненулевой адрес не делает вызов корректным, если C ожидает другую раскладку аргументов, соглашение о вызовах или способ передачи результата.
Тип параметра должен отражать обе допустимые ситуации:
None представляется нулевым указателем функции, а Some(handle_event) содержит адрес функции с ABI C. При вызове из Rust сначала нужно обработать Option: вызвать функцию можно только из ветки Some.
Само использование Option не проверяет контракт C. Нужно дополнительно подтвердить, что C действительно трактует NULL как отсутствие callback, не вызывает callback после нарушения его времени жизни и передаёт аргументы согласно объявленной сигнатуре.
Если C сохраняет callback для последующих вызовов, функция должна оставаться доступной всё это время. Обычная функция верхнего уровня или функция без захвата подходит лучше, чем замыкание: замыкание с состоянием нельзя напрямую представить обычным указателем на C-функцию.
Если C может вызывать callback из другого потока, отдельно проверяются потокобезопасность операции, состояние, захваченное через внешний контекст, и правила повторного входа. Option<extern "C" fn(...)> решает только представление nullable указателя, но не остальные инварианты FFI.
C-библиотека регистрирует обработчик событий: переданный callback вызывается при событии, а NULL отключает обработчик.
Вариант с обычным extern "C" fn(...) не выражает отключённое состояние. Использование нулевого адреса вручную через raw pointer требует ручной проверки перед каждым вызовом и легко приводит к ошибке.
Отдельный флаг enabled тоже неудобен: он может рассинхронизироваться с самим указателем, а C API всё равно ожидает один nullable callback. Преобразование целого числа в указатель на функцию ещё хуже — оно не доказывает корректность адреса и ABI.
Выбранный вариант — Option<extern "C" fn(...)>. Он совпадает с соглашением C API, делает отсутствие callback явным в типе и заставляет Rust-код обработать случай None. В результате отключение обработчика не требует фиктивной функции и не создаёт вызова нулевого адреса.
Option<fn(...)> без extern "C"?Нет. Здесь Option правильно моделирует отсутствие, но тип функции всё ещё использует Rust ABI. C должен получать указатель на функцию с тем ABI, который он будет использовать при вызове, поэтому нужен тип вроде Option<extern "C" fn(...)>.
Option<extern "C" fn(...)> безопасность самого вызова?Нет. Он гарантирует корректное представление двух состояний — Some и None, — но не проверяет фактическое поведение внешнего кода. C может передать неправильные аргументы, вызвать callback после завершения нужного контекста или нарушить требования к потокам. Эти условия должны быть частью контракта безопасной Rust-обёртки.
Напрямую — нет. Замыкание с захватами обычно содержит данные и не является обычным указателем на функцию C. Типичный FFI-дизайн передаёт отдельно extern "C" fn и непрозрачный контекстный указатель, а Rust-обёртка должна доказать его время жизни, корректное преобразование обратно в состояние и отсутствие недопустимого одновременного доступа.