FFI в Rust: вызов функций C из Rust
FFI — это Foreign Function Interface, «интерфейс внешних функций». Так Rust общается с кодом, написанным на другом языке. Почти всегда этот другой язык — C или что-то, что следует правилам C: C — это «общий язык», который понимают почти все языки и операционные системы.
Представь двух людей, у которых разные родные языки, но оба немного говорят по-английски. C и есть этот общий язык. С FFI можно пользоваться зрелой библиотекой на C из Rust или написать быструю библиотеку на Rust, которую смогут вызывать программы на Python, Node.js или C. Эта страница опирается на урок Unsafe, так что, если блоки unsafe для тебя в новинку, сначала прочитай его.
Вызываем функцию C
Заголовок раздела «Вызываем функцию C»Стандартная библиотека C всегда подключена к программам на Rust в Linux, macOS и Windows, поэтому её функции идеально подходят для первого опыта. Чтобы вызвать функцию, её нужно объявить в блоке extern "C": имя, параметры и тип возвращаемого значения. Потом её вызывают внутри unsafe:
use std::ffi::c_int;
unsafe extern "C" { // Это `int abs(int x);` из C, записанная на Rust. fn abs(x: c_int) -> c_int;}
fn main() { let n = unsafe { abs(-42) }; println!("abs(-42) = {n}");}Вывод:
abs(-42) = 42На что обратить внимание:
"C"— это название ABI, правил того, как передаются аргументы и возвращаются значения. Правила C — те, о которых договорились все.c_int— так в Rust называетсяintиз C. В модулеstd::ffiесть типы вродеc_int,c_longиc_char, чтобы твоё объявление точно совпадало с C на любой платформе.- Ты пишешь
unsafe extern "C". В редакции (edition) 2024 этотunsafeобязателен: ты обещаешь, что объявление корректно. В старом коде (и в редакции 2021) часто пишут простоextern "C" { ... }. Вариант сunsafeработает в обеих, поэтому на этой странице он используется везде.
Почему каждый вызов unsafe — и как это спрятать
Заголовок раздела «Почему каждый вызов unsafe — и как это спрятать»Компилятор Rust не умеет читать код на C. Он понятия не имеет, действительно ли abs принимает int, может ли она упасть и не сохранит ли она указатель, который ты ей передал. Поэтому каждый вызов внешней функции — unsafe: ответственность за правильное использование берёшь ты.
Хорошая новость: хорошенько подумать об этом нужно лишь один раз. Обычный приём — написать маленькую безопасную обёртку: обычную функцию на Rust, которая проверяет правила, а потом делает unsafe-вызов. Все остальные просто вызывают обёртку.
Вот настоящий пример. В C abs(INT_MIN) — это неопределённое поведение: ответ слишком велик для int. Наша обёртка сначала проверяет этот случай:
use std::ffi::c_int;
unsafe extern "C" { fn abs(x: c_int) -> c_int;}
// Безопасная обёртка: вызывающему коду не нужен `unsafe`.fn safe_abs(x: i32) -> Option<i32> { // У abs(i32::MIN) в C нет корректного ответа, поэтому мы отказываемся. if x == i32::MIN { return None; } // SAFETY: x не равен i32::MIN, поэтому у abs есть корректный результат. Some(unsafe { abs(x) })}
fn main() { println!("{:?}", safe_abs(-42)); println!("{:?}", safe_abs(i32::MIN));}Вывод:
Some(42)NoneКомментарий // SAFETY: — привычка Rust-разработчиков, которую стоит перенять. Он объясняет, почему именно этот блок unsafe в порядке, чтобы следующему читателю не пришлось гадать.
Если у функции C действительно нет никаких правил ни для каких входных данных, её можно пометить safe fn внутри блока unsafe extern, и тогда вызывать без unsafe. abs — не такая функция (вспомни INT_MIN), поэтому мы оставляем её unsafe и проверяем входные данные сами.
Строки: CString, CStr и байт NUL
Заголовок раздела «Строки: CString, CStr и байт NUL»Больше всего ошибок FFI живёт в строках, потому что Rust и C хранят их по-разному:
String/&strв Rust знает свою длину и может содержать любые байты, включая0.- Строка C — это просто указатель на байты. Строка заканчивается на первом байте
0(он называется NUL). Длина нигде не хранится.
Поэтому передать &str в C напрямую нельзя. Используй CString (на английском), чтобы создать собственную копию с NUL в конце, и .as_ptr(), чтобы получить *const c_char, который ждёт C. Строки в этом примере остаются на английском: результат зависит от количества байт.
use std::ffi::{c_char, CString};
unsafe extern "C" { fn strlen(s: *const c_char) -> usize;}
fn c_strlen(text: &str) -> Option<usize> { // CString::new сам добавляет NUL в конец. let c_text = CString::new(text).ok()?; // SAFETY: c_text — корректная строка с NUL на конце, и она живёт до конца этой функции. Some(unsafe { strlen(c_text.as_ptr()) })}
fn main() { let hello = CString::new("hello").unwrap(); println!("{:?}", hello.as_bytes_with_nul());
println!("{:?}", c_strlen("hello")); println!("{:?}", c_strlen("héllo"));
match CString::new("hel\0lo") { Ok(_) => println!("ok"), Err(e) => println!("ошибка: {e}"), }}Вывод:
[104, 101, 108, 108, 111, 0]Some(5)Some(6)ошибка: nul byte found in provided data at position: 3Здесь два сюрприза, и оба важны:
strlen("héllo")— это 6, а не 5. C считает байты, аéв UTF-8 занимает два байта. С кириллицей то же самое: каждая русская буква — два байта, так чтоstrlen("привет")вернёт 12.CString::newне срабатывает, если в тексте есть байт0. C решил бы, что строка там заканчивается, и молча отрезал бы"lo". Rust вместо этого отказывается — поэтомуCString::newи возвращаетResult. Текст самой ошибки (nul byte found in provided data at position: 3— «в данных найден байт NUL на позиции 3») стандартная библиотека печатает по-английски.
В обратную сторону, когда C отдаёт тебе char *, оберни его в CStr (на английском). CStr относится к CString так же, как &str к String: это заимствованный взгляд на данные. Функция C strchr возвращает указатель на первый найденный символ, и это хорошая демонстрация:
use std::ffi::{c_char, c_int, CStr, CString};
unsafe extern "C" { fn strchr(s: *const c_char, c: c_int) -> *mut c_char;}
fn find_from(text: &CStr, ch: u8) -> Option<String> { let ptr = unsafe { strchr(text.as_ptr(), ch as c_int) }; if ptr.is_null() { return None; // C говорит «не найдено» нулевым указателем } // SAFETY: ptr указывает внутрь `text`, а он жив и заканчивается NUL. let found = unsafe { CStr::from_ptr(ptr) }; Some(found.to_string_lossy().into_owned())}
fn main() { let text = CString::new("привет, мир").unwrap(); println!("{:?}", find_from(&text, b',')); println!("{:?}", find_from(&text, b'z'));
// Литерал c"..." — это &CStr, в котором NUL уже добавлен. println!("{:?}", find_from(c"rust-lang.org", b'.'));}Вывод:
Some(", мир")NoneSome(".org")to_string_lossy() превращает байты C в текст Rust. Любой некорректный UTF-8 он заменяет на �, потому что строки C не обещают быть в UTF-8. Если хочешь получить ошибку, используй .to_str().
Общие структуры с #[repr(C)]
Заголовок раздела «Общие структуры с #[repr(C)]»Rust разрешено переставлять поля структуры, чтобы сэкономить место. C так никогда не делает: поля лежат в памяти в том порядке, в каком ты их написал, с выравнивающими промежутками (padding) между ними. Например, структура с полями u8, u32, u8 сегодня занимает 8 байт с раскладкой Rust (Rust ставит два u8 рядом), но 12 байт с раскладкой C. Если C и Rust расходятся во мнении о раскладке, C читает не те байты — и ничто тебя не предупредит.
Поэтому, когда структура пересекает границу, попроси Rust использовать раскладку C с помощью #[repr(C)]. Функция C div возвращает структуру div_t с двумя полями int, quot и rem:
use std::ffi::c_int;
// Соответствует C: typedef struct { int quot; int rem; } div_t;#[repr(C)]#[derive(Debug)]struct DivT { quot: c_int, rem: c_int,}
unsafe extern "C" { fn div(numerator: c_int, denominator: c_int) -> DivT;}
fn main() { let result = unsafe { div(17, 5) }; println!("{result:?}"); println!("17 / 5 = {}, остаток {}", result.quot, result.rem);}Вывод:
DivT { quot: 3, rem: 2 }17 / 5 = 3, остаток 2Даём C вызывать Rust
Заголовок раздела «Даём C вызывать Rust»FFI работает в обе стороны. Напиши extern "C" fn (без блока) — и получишь функцию на Rust, которая следует правилам вызова C, так что код на C может её вызвать. Функция C qsort принимает функцию сравнения как аргумент — и мы можем дать ей функцию на Rust:
use std::ffi::{c_int, c_void};
unsafe extern "C" { fn qsort( base: *mut c_void, count: usize, size: usize, compare: extern "C" fn(*const c_void, *const c_void) -> c_int, );}
// Функция на Rust, которую будет вызывать C.extern "C" fn compare_i32(a: *const c_void, b: *const c_void) -> c_int { // SAFETY: qsort даёт нам только указатели внутрь нашего массива i32. let (a, b) = unsafe { (*(a as *const i32), *(b as *const i32)) }; a.cmp(&b) as c_int // Less = -1, Equal = 0, Greater = 1}
fn sort_with_c(numbers: &mut [i32]) { unsafe { qsort( numbers.as_mut_ptr() as *mut c_void, numbers.len(), std::mem::size_of::<i32>(), compare_i32, ); }}
fn main() { let mut numbers = [42, 7, 19, -3, 0]; sort_with_c(&mut numbers); println!("{:?}", numbers);}Вывод:
[-3, 0, 7, 19, 42](В реальном коде ты просто вызвал бы sort(). Это только демонстрация того, как C вызывает Rust обратно.)
Когда программе на C нужно найти твою функцию по имени, добавь #[no_mangle]. Обычно Rust «искажает» (mangle) имена во что-то вроде _ZN4main11add_numbers17h...E, чтобы разные крейты никогда не конфликтовали. #[no_mangle] сохраняет простое имя add_numbers:
#[no_mangle]pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 { a + b}Заметка о редакции 2024: примеры для Playground на этом сайте используют редакцию 2021, где #[no_mangle] допустим. В редакции 2024 (её по умолчанию получает новый проект из cargo new) его нужно писать как #[unsafe(no_mangle)]: две функции с одинаковым неискажённым именем сломали бы программу. Новое написание работает и в редакции 2021.
Чтобы собрать библиотеку, к которой может подключиться C, укажи в Cargo.toml тип крейта cdylib:
[lib]crate-type = ["cdylib"]// src/lib.rs (редакция 2024)#[unsafe(no_mangle)]pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 { a + b}cargo build --release создаёт для крейта с именем mathlib файл target/release/libmathlib.so (.dylib в macOS, .dll в Windows). Программа на C объявляет функцию и линкуется с библиотекой:
#include <stdio.h>#include <stdint.h>
int32_t add_numbers(int32_t a, int32_t b);
int main(void) { printf("2 + 3 = %d\n", add_numbers(2, 3)); return 0;}$ cc main.c -L target/release -lmathlib -o main$ LD_LIBRARY_PATH=target/release ./main2 + 3 = 5Писать такой заголовочный файл C вручную быстро надоедает. cbindgen (на английском) генерирует его из твоего кода на Rust.
Компилируем свой код на C с build.rs и cc
Заголовок раздела «Компилируем свой код на C с build.rs и cc»До сих пор мы использовали только функции, которые уже подключены. Чтобы использовать свой файл на C, пусть его скомпилирует Cargo. Файл build.rs в корне проекта — это скрипт сборки (build script): Cargo запускает его перед компиляцией твоего крейта. Крейт cc (на английском) сводит всё к нескольким строкам.
cargo new hello-ccd hello-ccargo add --build cc#include <stdio.h>
int add_ints(int a, int b) { return a + b;}
void greet(const char *name) { printf("Hello from C, %s!\n", name); fflush(stdout);}fn main() { // Компилируем src/greet.c в статическую библиотеку и подключаем её. cc::Build::new().file("src/greet.c").compile("greet"); // Перезапускать этот скрипт, только когда меняется файл на C. println!("cargo::rerun-if-changed=src/greet.c");}use std::ffi::{c_char, c_int, CString};
unsafe extern "C" { fn add_ints(a: c_int, b: c_int) -> c_int; fn greet(name: *const c_char);}
fn main() { let sum = unsafe { add_ints(2, 3) }; println!("2 + 3 = {sum}");
let name = CString::new("Ferris").unwrap(); unsafe { greet(name.as_ptr()) };}$ cargo run -q2 + 3 = 5Hello from C, Ferris!Нужен установленный компилятор C (gcc, clang или MSVC в Windows); cc найдёт его сам.
Для большой библиотеки на C с сотнями функций писать каждое объявление extern вручную долго, и легко ошибиться. bindgen (на английском) читает заголовочные файлы C и генерирует объявления на Rust за тебя — обычно тоже из build.rs. Большинство крейтов -sys на crates.io (например, openssl-sys) устроены именно так.
Не только C: Python, Node.js и веб
Заголовок раздела «Не только C: Python, Node.js и веб»Раз C — общий язык, те же идеи позволяют Rust общаться со многими другими языками. Для них редко пишут голый код extern "C": всю unsafe-работу делает крейт и даёт тебе безопасный, удобный API:
- Python: PyO3 (на английском) позволяет писать модули Python на Rust (и вызывать Python из Rust). Сопутствующий инструмент
maturinсобирает и публикует их как обычные пакетыpip. Его используют, например, Polars и pydantic-core. - Node.js: napi-rs (на английском) собирает нативные дополнения для Node.js на Rust и сам генерирует типы TypeScript.
- Проекты на C и C++:
cdylibплюс cbindgen (на английском), как показано выше. - Браузер: это не FFI в смысле C, но похожую роль играет
wasm-bindgen. См. Rust и WebAssembly.
Частые ошибки
Заголовок раздела «Частые ошибки»Вызывать внешнюю функцию вне unsafe.
unsafe extern "C" { fn abs(x: i32) -> i32;}
fn main() { let n = abs(-7); println!("{n}");}error[E0133]: call to unsafe function `abs` is unsafe and requires unsafe function or block --> src/main.rs:6:13 |6 | let n = abs(-7); | ^^^^^^^ call to unsafe functionКомпилятор говорит, что вызов unsafe-функции требует unsafe-функции или блока. Исправление: оберни вызов в unsafe { ... }, а ещё лучше — в безопасную функцию-обёртку, как показано выше.
Указатель на CString, которой уже нет. Это компилируется, но это настоящая ошибка:
use std::ffi::{c_char, CString};
unsafe extern "C" { fn strlen(s: *const c_char) -> usize;}
fn main() { let ptr = CString::new("hello").unwrap().as_ptr(); let len = unsafe { strlen(ptr) }; println!("{len}");}warning: this creates a dangling pointer because temporary `CString` is dropped at end of statement --> src/main.rs:8:46 |8 | let ptr = CString::new("hello").unwrap().as_ptr(); | ------------------------------ ^^^^^^ pointer created here | | | this `CString` is dropped at end of statement | = help: bind the `CString` to a variable such that it outlives the pointer returned by `as_ptr`Предупреждение говорит, что получился висячий указатель: временная CString удаляется в конце инструкции. То есть она освобождается на ;, и ptr теперь указывает на освобождённую память. Исправление: держи CString в отдельной переменной (let s = CString::new("hello").unwrap();) и вызывай s.as_ptr(), пока s жива. Никогда не игнорируй это предупреждение.
Простой extern "C" в проекте с редакцией 2024. Код, скопированный из старых руководств, падает так:
error: extern blocks must be unsafe --> src/main.rs:3:1 |3 | / extern "C" {4 | | fn add_ints(a: c_int, b: c_int) -> c_int;5 | | fn greet(name: *const c_char);6 | | } | |_^Ошибка значит «блоки extern должны быть unsafe». Исправление: пиши unsafe extern "C" { ... }.
Забыть #[repr(C)]. Никакой ошибки нет вообще — C просто читает мусор. Ставь #[repr(C)] на каждую структуру и каждое перечисление, которые пересекают границу.
- Объявляй функции C в блоке
unsafe extern "C" { ... }, используя типы C изstd::ffi, такие какc_intиc_char. - Каждый внешний вызов —
unsafe. Оберни его один раз в безопасную функцию на Rust, которая проверяет правила, с комментарием// SAFETY:. - Строки C заканчиваются байтом NUL.
CStringнужен, чтобы отправить текст в C, аCStr— чтобы прочитать текст из C. ДержиCStringживой, пока C пользуется указателем. - Используй
#[repr(C)]для структур, общих с C, потому что Rust может переставлять поля. extern "C" fnплюс#[no_mangle](#[unsafe(no_mangle)]в редакции 2024) позволяют C вызывать Rust;cdylibсобирает для этого библиотеку.build.rs+ccкомпилируют твой код на C;bindgenгенерирует объявления; PyO3 и napi-rs связывают Rust с Python и Node.js.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Почему каждый вызов функции из блока extern "C" unsafe?
2. Что произойдёт при CString::new("hel\0lo")?
3. Зачем структурам, общим с C, нужен #[repr(C)]?
4. Как в крейте с редакцией 2024 записать атрибут, который сохраняет для C простое имя функции?
Результат: 0 / 4
Что почитать
Заголовок раздела «Что почитать»- FFI — The Rustonomicon (на английском): официальное глубокое погружение — обратные вызовы, линковка, нулевые указатели и многое другое.
std::ffi— документация стандартной библиотеки (на английском):CString,CStr,c_int,c_charи их друзья, с примерами.- External blocks — The Rust Reference (на английском): точные правила для блоков
extern, ABI и элементовsafe/unsafe. - The bindgen User Guide (на английском): генерация привязок Rust из заголовочных файлов C.
- The PyO3 User Guide (на английском): как писать расширения Python на Rust.