Сегодня Rust активно используется не только как язык для написания приложений, в том числе системных, но и как язык для написания библиотек, которые подключают к существующим проектам на C и C++.
Это удобно: новую функциональность можно писать на Rust, но при этом не переписывать весь код на нём.
В этой статье я покажу, как:
написать библиотеку на Rust;
выставить для неё C API через FFI;
собрать всё одной командой через CMake;
использовать её из C++ кода;
реализовать обратные вызовы (callback), которые Rust будет вызывать в C++.
Весь код, части которого приведены в статье, доступен в репозитории:
github.com/gvtret/rust-ffi-demo
Зачем вообще FFI?
FFI (Foreign Function Interface) — это механизм, позволяющий коду на одном языке вызывать функции, написанные на другом.
В нашем случае — Rust экспортирует функции в стиле C, а C++ может их вызывать.
Почему так?
Совместимость: C ABI (Application Binary Interface) поддерживается любым языком.
Простота: вместо «магии» — обычные функции
extern "C"
.Контроль: программист сам решает, какие структуры и методы показывать снаружи.
Структура проекта
Мы построили проект так, чтобы Rust был чистым ядром, а всё взаимодействие с C++ шло через отдельный FFI-слой:
.
├─ Cargo.toml # Cargo manifest (Rust)
├─ cbindgen.toml # Config for generating C headers
├─ CMakeLists.txt # Unified build (Rust + C++)
├─ src/
│ ├─ core.rs # Pure Rust implementation (Counter)
│ ├─ ffi.rs # FFI wrappers (extern "C")
│ └─ lib.rs # Entry point, re-exports
├─ cpp/
│ └─ main.cpp # C++ usage example
└─ build/ # Build artifacts
Чистая логика (Rust)
В ядре у нас простая структура Counter
: счётчик с меткой и возможностью назначить колбэк.
pub struct Counter {
value: i64,
label: Option<String>,
callback: Option<Box<dyn FnMut(i64)>>,
}
impl Counter {
pub fn new(initial: i64) -> Self { ... }
pub fn increment(&mut self, delta: i64) { ... }
pub fn reset(&mut self) { ... }
pub fn value(&self) -> i64 { ... }
pub fn set_label(&mut self, s: Option<String>) { ... }
pub fn label(&self) -> Option<&str> { ... }
pub fn set_callback(&mut self, cb: Option<Box<dyn FnMut(i64)>>) { ... }
}
Особенность: при изменении значения (increment
, reset
) вызывается callback, если он установлен.
FFI-слой (Rust → C)
Rust напрямую в C++ не экспортируется. Нужен «мост» — функции extern "C"
.
Пример:
#[repr(C)]
pub enum RustFfiDemoStatus {
RustffiOk = 0,
RustffiNullArg = 1,
RustffiInvalidArg = 2,
RustffiInternalError = 3,
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_ffi_demo_counter_new(initial: i64, out: *mut *mut CounterHandle) -> RustFfiDemoStatus { ... }
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_ffi_demo_counter_increment(handle: *mut CounterHandle, delta: i64) -> RustFfiDemoStatus { ... }
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_ffi_demo_counter_set_callback(handle: *mut CounterHandle, cb: CounterCallback) -> RustFfiDemoStatus { ... }
Здесь важно:
#[unsafe(no_mangle)]
— экспортируем функцию с фиксированным именем, понятным для C++.extern "C"
— ABI в стиле C.Все ошибки кодируем через enum
RustFfiDemoStatus
.
Генерация заголовка для C++
Чтобы C++ понимал Rust API, нужен C-заголовок.
Мы используем cbindgen.
Конфигурация cbindgen.toml
:
language = "C"
header = "/* Generated automatically by cbindgen. Do not edit. */"
include_guard = "RUST_FFI_DEMO_H"
pragma_once = true
cpp_compat = true
usize_is_size_t = true
[export]
include = ["CounterHandle", "RustFfiDemoStatus"]
[parse]
exclude = ["Counter"]
[defines]
"target_os = windows" = "RUST_FFI_DEMO_WINDOWS"
"target_os = linux" = "RUST_FFI_DEMO_LINUX"
"target_os = macos" = "RUST_FFI_DEMO_MACOS"
Теперь при сборке генерируется корректный rust_ffi_demo.h
.
Сборка через CMake
Весь проект собирается одной командой:
mkdir -p build
cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
make
CMake выполняет 3 задачи:
Rust собирается через
cargo build
→ библиотека.so
/.dll
.cbindgen
генерируетrust_ffi_demo.h
.C++ код линкуется с Rust-библиотекой.
Использование в C++
Простой пример:
#include "rust_ffi_demo.h"
#include <iostream>
int main() {
CounterHandle* counter = nullptr;
rust_ffi_demo_counter_new(10, &counter);
rust_ffi_demo_counter_increment(counter, 5);
long long value = 0;
rust_ffi_demo_counter_value(counter, &value);
std::cout << "Counter value = " << value << std::endl;
rust_ffi_demo_counter_free(counter);
}
Callback: Rust вызывает C++
Самое интересное: теперь Rust может сам вызывать функцию, реализованную в C++.
Rust
pub type CounterCallback = Option<unsafe extern "C" fn(value: i64)>;
#[unsafe(no_mangle)]
pub unsafe extern "C" fn rust_ffi_demo_counter_set_callback(
handle: *mut CounterHandle,
cb: CounterCallback,
) -> RustFfiDemoStatus {
let c = match as_counter_mut(handle) {
Ok(c) => c,
Err(e) => return e,
};
if let Some(func) = cb {
c.set_callback(Some(Box::new(move |val| {
unsafe { func(val) };
})));
} else {
c.set_callback(None);
}
RustFfiDemoStatus::RustffiOk
}
C++
void on_value_changed(int64_t value) {
std::cout << "[C++] Callback fired! New value = " << value << std::endl;
}
...
rust_ffi_demo_counter_set_callback(counter, on_value_changed);
rust_ffi_demo_counter_increment(counter, 7); // fires callback
rust_ffi_demo_counter_reset(counter); // fires callback
Вывод:
Counter value = 15
[C++] Callback fired! New value = 22
[C++] Callback fired! New value = 0
Какие проблемы могут возникнуть
1. Сырые указатели
FFI всегда работает с *mut T
/ *const T
. Любая ошибка (null, double free, dangling) → UB.
Решение: в Rust проверять указатели, возвращать статус-коды.
2. Жизненный цикл объектов
C++ должен вызывать free
для объектов, созданных Rust.
Решение: явные функции *_new
и *_free
.
3. Callback и владение
В Rust Box<dyn FnMut>
нельзя клонировать, а C++ передаёт «голый» указатель на функцию.
Решение: в Clone
обнуляем callback, в Debug скрываем его.
4. Отладка
C++ вызывает Rust, Rust вызывает C++. Легко потеряться в стеках.
Решение: использовать CodeLLDB
и Rust debug info (cargo build
без --release
).
5. Совместимость ABI
Rust и C++ могут отличаться по выравниванию структур и enum.
Решение: всегда использовать #[repr(C)]
для типов, экспортируемых в API.
Отладка в VSCode
В .vscode/launch.json
:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug cpp_example + Rust",
"type": "lldb",
"request": "launch",
"program": "${workspaceFolder}/build/cpp_example",
"cwd": "${workspaceFolder}/build",
"env": {
"LD_LIBRARY_PATH": "${workspaceFolder}/target/debug"
},
"sourceLanguages": ["rust", "cpp"]
}
]
}
Что дальше?
Поддержка нескольких колбэков (подписки).
Асинхронные события (через каналы/потоки).
Установка библиотеки через
make install
.Пакетирование под Linux/Windows/macOS.
Пример с реальным приложением (например, GUI на Qt + бизнес-логика на Rust).
Заключение
Rust и C++ можно интегрировать красиво:
Rust даёт надёжность и безопасность.
FFI через
extern "C"
делает API простым и понятным.CMake связывает всё в единую сборку.
Колбэки позволяют Rust и C++ взаимодействовать в обе стороны.
Исходники проекта доступны на GitHub:
github.com/gvtret/rust-ffi-demo
Лицензия
MIT
rsashka
Во первых функции не С++, а С, а во вторых куда делись С++ исключения?