Допустимо ли передавать указатель на dyn Trait через FFI как обычный указатель на C-структуру?
Нет. Указатель на dyn Trait содержит не только адрес данных, но и метаданные динамической диспетчеризации, обычно указатель на таблицу виртуальных методов. Формат этого указателя, layout vtable и ABI методов не являются стабильным C-контрактом, поэтому такой объект нельзя напрямую интерпретировать в C как обычную структуру.
Трейт-объекты появились как механизм динамического полиморфизма и сокрытия конкретного типа: Rust хранит объект и сведения, необходимые для вызова методов через dyn Trait. Это удобно внутри Rust, где компилятор согласует представление указателя и вызовы методов.
FFI решает другую задачу: он требует явно согласованного представления данных, calling convention и правил владения. Поэтому Rust не обещает стабильный ABI для обычных Rust-типов и внутренних структур trait objects.
Указатель на dyn Trait является широким указателем: кроме адреса объекта он несёт метаданные. Если передать его в C-функцию, ожидающую указатель на структуру, C не знает, где находятся эти компоненты, как устроена vtable и каким ABI вызывать методы.
Даже если конкретная версия компилятора использует ожидаемое представление, это не делает его публичным контрактом. Изменение компилятора, типа трейта или параметров методов может привести к чтению неверных адресов, вызову функции с несовместимой сигнатурой и неопределённому поведению.
На границе FFI нужно передавать только типы с явно согласованным ABI: значения с repr(C), указатели на непрозрачные объекты, примитивы и функции с подходящим extern "C" ABI. Trait object следует оставить внутри Rust, а C предоставить узкий адаптер.
Один из вариантов — передать C непрозрачный handle на размещённый в Rust объект. C может хранить и возвращать этот handle, но не должен разыменовывать его как структуру или пытаться самостоятельно вызывать методы. Rust-обёртка обязана определить срок жизни объекта, владение, допустимость null и потокобезопасность.
Другой вариант — описать явную таблицу операций с repr(C). В ней находятся C-совместимые указатели на функции и явный контекст. Такой подход стабилизирует только объявленный контракт: каждая функция должна иметь extern "C", а layout таблицы, порядок полей и правила владения должны быть документированы.
В этом примере C видит только стабильную структуру с контекстом и функцией. Rust может преобразовать контекст обратно к своему типу только при доказанном происхождении указателя, корректном сроке жизни и отсутствии нарушений алиасинга. Само наличие unsafe extern "C" не проверяет эти инварианты автоматически.
Rust-библиотека предоставляет C-плагинам сервис с несколькими операциями. Первый вариант — экспортировать указатель на Box<dyn Service> и предложить C вызывать его методы. Он минимален по объёму кода, но непригоден как ABI-контракт: C не знает layout trait object, а Rust не гарантирует layout vtable.
Второй вариант — экспортировать набор отдельных функций Rust с параметром-указателем на контекст. Он лучше контролирует ABI, но требует ручного описания каждой операции и проверки контекста в каждом адаптере.
Выбранный вариант — repr(C)-таблица функций с непрозрачным контекстом. Для каждой функции фиксируются ABI, допустимые значения указателей, владение и правила уничтожения объекта. В результате C не зависит от внутреннего представления Rust, а реализацию сервиса можно менять без изменения FFI-контракта.
repr(C) у конкретного типа layout указателя на dyn Trait или его vtable?Нет. repr(C) задаёт C-представление для применимого конкретного типа, но не превращает внутреннюю vtable trait object в публичную C-структуру. Он также не фиксирует ABI методов трейта и не делает сам dyn Trait пригодным для передачи через FFI.
Box<dyn Trait> как непрозрачный handle?Да, если C получает именно непрозрачный указатель и не интерпретирует его содержимое. Rust должен сохранить владение объектом или явно передать его по контракту, не допустить использование после уничтожения и обеспечить корректное преобразование handle обратно в исходный тип.
Это отличается от передачи самого trait object: C хранит адрес handle, но не получает права читать данные объекта или вызывать методы через неизвестную ему vtable.
Нет. Нужно совпадение ABI, представления параметров и результата, layout самой таблицы, правил владения и допустимого состояния контекста. Функция с обычным Rust ABI не становится совместимой с C только потому, что её параметры имеют те же типы.
Кроме того, Rust-код должен доказать, что указатель функции не null, контекст имеет ожидаемый тип и срок жизни, а вызов не нарушает правила повторного входа и многопоточности.