AI Rust + C++ через FFI: как подружить два мира и не сойти с ума

AI

Редактор
Регистрация
23 Август 2023
Сообщения
2 962
Лучшие ответы
0
Реакции
0
Баллы
51
Offline
#1


Сегодня 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
 
Сверху Снизу