Перейти к содержимому

FFI в Rust: вызов функций C из Rust

FFI — это Foreign Function Interface, «интерфейс внешних функций». Так Rust общается с кодом, написанным на другом языке. Почти всегда этот другой язык — C или что-то, что следует правилам C: C — это «общий язык», который понимают почти все языки и операционные системы.

Представь двух людей, у которых разные родные языки, но оба немного говорят по-английски. C и есть этот общий язык. С FFI можно пользоваться зрелой библиотекой на C из Rust или написать быструю библиотеку на Rust, которую смогут вызывать программы на Python, Node.js или C. Эта страница опирается на урок Unsafe, так что, если блоки unsafe для тебя в новинку, сначала прочитай его.

Стандартная библиотека 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 и проверяем входные данные сами.

Больше всего ошибок 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
▶ Попробуй сам

Здесь два сюрприза, и оба важны:

  1. strlen("héllo") — это 6, а не 5. C считает байты, а é в UTF-8 занимает два байта. С кириллицей то же самое: каждая русская буква — два байта, так что strlen("привет") вернёт 12.
  2. 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(", мир")
None
Some(".org")
▶ Попробуй сам

to_string_lossy() превращает байты C в текст Rust. Любой некорректный UTF-8 он заменяет на �, потому что строки C не обещают быть в UTF-8. Если хочешь получить ошибку, используй .to_str().

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
▶ Попробуй сам

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;
}
Terminal window
$ cc main.c -L target/release -lmathlib -o main
$ LD_LIBRARY_PATH=target/release ./main
2 + 3 = 5

Писать такой заголовочный файл C вручную быстро надоедает. cbindgen (на английском) генерирует его из твоего кода на Rust.

До сих пор мы использовали только функции, которые уже подключены. Чтобы использовать свой файл на C, пусть его скомпилирует Cargo. Файл build.rs в корне проекта — это скрипт сборки (build script): Cargo запускает его перед компиляцией твоего крейта. Крейт cc (на английском) сводит всё к нескольким строкам.

Terminal window
cargo new hello-c
cd hello-c
cargo add --build cc
src/greet.c
#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);
}
build.rs
fn main() {
// Компилируем src/greet.c в статическую библиотеку и подключаем её.
cc::Build::new().file("src/greet.c").compile("greet");
// Перезапускать этот скрипт, только когда меняется файл на C.
println!("cargo::rerun-if-changed=src/greet.c");
}
src/main.rs
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()) };
}
Terminal window
$ cargo run -q
2 + 3 = 5
Hello from C, Ferris!

Нужен установленный компилятор C (gcc, clang или MSVC в Windows); cc найдёт его сам.

Для большой библиотеки на C с сотнями функций писать каждое объявление extern вручную долго, и легко ошибиться. bindgen (на английском) читает заголовочные файлы C и генерирует объявления на Rust за тебя — обычно тоже из build.rs. Большинство крейтов -sys на crates.io (например, openssl-sys) устроены именно так.

Раз 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