FFI на практике в Rust
FFI на практике в Rust
Что такое FFI и зачем он нужен
FFI (Foreign Function Interface) — способ вызывать код, собранный другим компилятором (чаще всего C или C++), из Rust и наоборот. Так подключают:
- системные библиотеки ОС (
libc); - старые SDK и DLL;
- драйверы и игровые движки;
- ускоренные куски на C внутри Rust-проекта.
На границе языков компилятор Rust перестаёт проверять контракт: неверный тип, «битый» указатель или несовпадение соглашения о вызовах — неопределённое поведение (как в C). Поэтому весь unsafe сжимают в тонкий слой, а наружу отдают обычный safe API с Result, документированными предусловиями и тестами.
Концепции: справочник, FFI · системное программирование · сборка: Cargo и build.rs.
Два направления
| Направление | Задача |
|---|---|
| C → Rust | В Rust объявляют extern "C" { fn c_func(...); } и линкуют .o / .a через build.rs |
| Rust → C | Экспортируют #[no_mangle] pub extern "C" fn ... и собирают cdylib |
extern "C" — соглашение о вызове (ABI): как аргументы лежат в регистрах/стеке. Это стабильный контракт для линковщика; речь о «диалекте C» как о бинарном интерфейсе, а не о синтаксисе языка.
Минимальный пример — вызов C из Rust
Шаг 1. Функция на C
native/add.c:
int add(int a, int b) {
return a + b;
}
Шаг 2. Сборка C в build.rs
fn main() {
cc::Build::new().file("native/add.c").compile("native_add");
println!("cargo:rerun-if-changed=native/add.c");
}
Crate cc вызывает системный компилятор C и сообщает Cargo имя статической библиотеки native_add для линковки.
Cargo.toml:
[build-dependencies]
cc = "1"
Шаг 3. Объявление и safe-обёртка в Rust
src/lib.rs:
use std::ffi::c_int;
extern "C" {
fn add(a: c_int, b: c_int) -> c_int;
}
/// Safe-обёртка: контракт C-функции проверен вручную один раз.
pub fn add_safe(a: i32, b: i32) -> i32 {
unsafe { add(a, b) }
}
Разбор:
- Блок
extern "C" { ... }— «эти символы придут из нативной библиотеки». - Вызов
add(...)внутриunsafe { }— программист берёт на себя ответственность за корректность сигнатуры. add_safe— то, что видят остальные модули: обычная функция безunsafeу вызывающего.
Правила хорошей обёртки:
- задокументировать предусловия (указатель не null, длина буфера достаточна);
- наружу не отдавать «голые»
*mut Tс неясным временем жизни; - коды ошибок C переводить в
Result.
Проверка:
cargo test
Структуры — #[repr(C)]
Rust по умолчанию может переупорядочить поля struct для оптимизации. C ожидает фиксированный layout в памяти.
#[repr(C)]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[repr(C)] — порядок и выравнивание полей как у C-структуры. Без этого FFI с C сломается молча.
Строки через границу
Тип &str в Rust нельзя «просто передать» в C: у C-строки другой формат (часто указатель на байты + соглашение про NUL-терминатор).
Используют:
CString— владеющая копия с завершающим\0для передачи в C;CStr— просмотр C-строки по*const c_charпри вызове из C.
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
pub fn to_c_string(s: &str) -> CString {
CString::new(s).expect("строка без внутреннего NUL")
}
pub unsafe fn from_c_str(ptr: *const c_char) -> Option<&'static str> {
if ptr.is_null() {
return None;
}
CStr::from_ptr(ptr).to_str().ok()
}
CString::newвернёт ошибку, если внутриsуже есть байт0— в C это конец строки.from_c_strпомеченunsafe: вы обязаны знать, чтоptrвалиден и живёт достаточно долго. Lifetime'staticоправдан только если C гарантирует вечное хранение (литерал, статический буфер); иначе lifetime должен быть параметром вашей обёртки.
Экспорт Rust для C
src/lib.rs:
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]— имя символа в.so/.dllбудетrust_add, а не искажённое Rust-имя.pub extern "C"— функция видна линковщику с C-ABI.
Cargo.toml:
[lib]
crate-type = ["cdylib", "rlib"]
cdylib— динамическая библиотека для линковки из C/C++.rlib— чтобы тот же crate можно было использовать из Rust.
В C объявляют:
int rust_add(int a, int b);
Паника через FFI — неопределённое поведение для вызывающего C. На границе ловят catch_unwind или возвращают код ошибки; C-стек нельзя «ронять» паникой Rust.
bindgen — заголовки .h → Rust
Для больших SDK вручную не пишут сотни extern. bindgen читает .h и генерирует модуль с объявлениями.
build.rs (упрощённо):
fn main() {
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("bindgen failed");
let out = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out.join("bindings.rs"))
.unwrap();
}
lib.rs:
mod ffi {
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}
Сгенерированный код остаётся unsafe. Поверх него пишут ручные safe-типы-обёртки: bindgen не делает API безопасным автоматически.
cbindgen — Rust → заголовок C
Если ядро на Rust, а потребители на C++, cbindgen сканирует pub extern "C" и генерирует .h для их компилятора.
Ошибки на границе (что ломается чаще всего)
| Проблема | Последствие |
|---|---|
Неверный repr или размер поля | чтение «чужой» памяти |
| Use-after-free указателя | UB |
| Паника в callback, вызванном из C | крах процесса |
free в C для памяти, выделенной аллокатором Rust | повреждение кучи |
Решение: явные пары create / destroy, документированный владелец буфера, симметричные Box::into_raw / Box::from_raw только в одном модуле.
Windows и COM (обзор)
Для Win32 и WinRT используют crate windows / windows-sys — тысячи extern уже сгенерированы. Прикладной код всё равно оборачивает в safe-типы.
Выбор GUI-стека — GUI на Windows.
Чек-лист перед merge
- Весь
unsafeв одном-двух модулях с тестами. build.rsобъявляетrerun-if-changedдля исходников C.- CI собирает нужные платформы (Linux / Windows / macOS).
- Для критичных обёрток — sanitizers / miri там, где применимо.
- В документации указано: кто освобождает память и из какого потока можно вызывать API.
Связанные материалы
См. также
Другие статьи этого же раздела в боковом меню (как на странице «О разделе»). История Rust - путь языка от эксперимента до промышленного стандарта безопасного системного программирования. Rust — это многопарадигменный язык программирования общего назначения, который фокусируется на безопасности памяти, производительности и параллелизме. Набор советов, правил, принципов и обычаев в разработке на этом языке. Трейты могут иметь методы по умолчанию. Если тип не переопределяет метод, используется версия из трейта. Это позволяет расширять функциональность без изменения базового кода. Простые приложения на Rust — CLI, файлы, JSON и минимальный HTTP на stdlib и tokio. fn - ключевое слово, которое обозначает начало объявления функции. Функция представляет собой именованный блок кода, выполняющий конкретную задачу. В данном случае функция называется main. Экосистема приложений на Rust - направления применения языка и ключевые инструменты промышленной разработки. Системное программирование на Rust - низкоуровневый контроль, безопасность памяти и надёжные инфраструктурные компоненты. Кавычки, точки, запятые, скобки и прочие знаки препинания. Ключевые слова Rust - назначение операторов и зарезервированных идентификаторов в синтаксисе языка. Встроенные функции и стандартная библиотека Rust - базовые макросы, атрибуты и инструменты тестирования. Типизация, набор правил определения типа данных значений языка.История языка Rust
Что требуется знать перед началом изучения языка программирования Rust
Рекомендации по разработке на Rust
Rust для начинающих
Простые приложения на Rust
Основы языка Rust
Экосистема приложений на Rust
Системное программирование на Rust
Синтаксис и пунктуация в Rust
Ключевые слова языка Rust
Встроенные функции и стандартная библиотека
Типы данных и владение памятью