Сегодня 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

Комментарии (1)


  1. rsashka
    05.10.2025 07:19

    Во первых функции не С++, а С, а во вторых куда делись С++ исключения?