FFI: как создать мост между Rust и C/C++

от автора

Привет, Хабр!

Сегодня мы рассмотрим, как создать безопасные FFI-интерфейсы в Rust для интеграции с C/C++ библиотеками

Если говорить проще, FFI (foreign function interface — интерфейс вызова внешних функций) – это способ «позаимствовать» функциональность из другого языка. В контексте нашей статьи, с одной стороны у нас Rust, где каждый байт памяти охраняется компилятором, а на другой C++, где свобода обращения с памятью может обернуться утечками или, что еще хуже, непредсказуемым UB (англ. undefined behavior, в ряде источников неопределенное поведение). И наша задача – сделать так, чтобы эти два мира не конфликтовали, а работали в унисон.

Итак, что можно сделать с помощью FFI?

  • Подключить legacy-код: зачем переписывать десятки тысяч строк, если можно просто обернуть проверенные временем C/C++ функции в безопасный Rust-API? Например, взять библиотеку для обработки изображений или шифрования и подключить ее, не переживая об утечках памяти.

  • Использовать системные API: некоторые системные вызовы или аппаратные фишки доступны только через C. С FFI легко подключишь их к своему Rust-приложению.

  • Интегрировать C++ классы: даже если есть сложные C++ классы с кучей методов и состоянием, можно создать C-совместимые обертки и подключить их в Rust.

Основы FFI в Rust: разбор первичных конструкций

В Rust для взаимодействия с внешними библиотеками используется ключевое слово extern. Объявляя функцию с extern «C», мы говорим компилятору: «Смотри, тут код из языка». Типы данных из модуля std::os::raw помогают сохранить совместимость с C.

Рассмотрим базовый пример: вызов функции сложения, реализованной на C.

/* add.c - простая функция сложения */ #include <stdio.h>  int add(int a, int b) {     return a + b; }

Собираем библиотеку:

gcc -c add.c -o add.o ar rcs libadd.a add.o

А теперь – Rust-код:

// main.rs - интеграция с C через FFI. use std::os::raw::c_int;  extern "C" {     /// функция для сложения двух чисел.     fn add(a: c_int, b: c_int) -> c_int; }  /// безопасная обертка вокруг небезопасного вызова C-функции `add`. pub fn safe_add(a: i32, b: i32) -> i32 {     // unsafe-блок локализован, чтобы минимизировать область возможных ошибок.     unsafe { add(a as c_int, b as c_int) as i32 } }  fn main() {     let a = 42;     let b = 58;     let result = safe_add(a, b);     println!("{} + {} = {}", a, b, result); }  

Здесь главное – свести unsafe до минимума, чтобы остальной код оставался чистым и безопасным.

Работа с ресурсами

Допустим, функция на C выделяет память и возвращает указатель на структуру. Если не обернуть это в RAII-модель Rust, рискуете оказаться с утечками памяти.

C-код (resource.c):

#include <stdlib.h> #include <string.h>  typedef struct {     char *data;     int length; } Resource;  /// создаeт ресурс, копируя строку. Resource* create_resource(const char* init_str) {     Resource* res = (Resource*)malloc(sizeof(Resource));     if (!res) return NULL;     res->length = (int)strlen(init_str);     res->data = (char*)malloc(res->length + 1);     if (!res->data) {         free(res);         return NULL;     }     strcpy(res->data, init_str);     return res; }  /// имитация использования ресурса. void use_resource(Resource* res) {     if (res && res->data) {         // тут какая-нибудь логика.     } }  /// освобождает ресурс. void free_resource(Resource* res) {     if (res) {         free(res->data);         free(res);     } }

Обертка на Rust:

use std::ffi::CString; use std::os::raw::c_char;  /// представление C-структуры Resource. Тип объявлен как opaque. #[repr(C)] pub struct Resource {     _private: [u8; 0], }  extern "C" {     /// создает ресурс.     fn create_resource(init_str: *const c_char) -> *mut Resource;     /// использует ресурс.     fn use_resource(res: *mut Resource);     /// освобождает ресурс.     fn free_resource(res: *mut Resource); }  /// RAII-обeртка для управления ресурсом. pub struct ResourceWrapper {     ptr: *mut Resource, }  impl ResourceWrapper {     /// создает новый ресурс или возвращает None, если что-то пошло не так.     pub fn new(initial: &str) -> Option<Self> {         let c_str = CString::new(initial).ok()?;         let res_ptr = unsafe { create_resource(c_str.as_ptr()) };         if res_ptr.is_null() {             None         } else {             Some(Self { ptr: res_ptr })         }     }      /// вызывает функцию использования ресурса.     pub fn use_it(&self) {         unsafe { use_resource(self.ptr) }     } }  impl Drop for ResourceWrapper {     fn drop(&mut self) {         if !self.ptr.is_null() {             unsafe { free_resource(self.ptr) }         }     } }  #[cfg(test)] mod tests {     use super::*;      #[test]     fn test_resource_wrapper() {         let resource = ResourceWrapper::new("Hello, Rust FFI")             .expect("Не удалось создать ресурс");         resource.use_it();         // free_resource вызывается автоматически при выходе из области видимости.     } }  

Применили паттерн RAII, чтобы автоматизировать освобождение памяти. Даже если функция create_resource вернет NULL, код корректно обработает эту ситуацию.

Интеграция с C++

Интеграция с C++ сложнее из-за name mangling и исключений. Чаще всего, для упрощения интеграции, создают обертки на C, скрывающие все сложности C++.

Пример C++ класса и его обертки

C++ заголовок (my_cpp_class.hpp):

#ifndef MY_CPP_CLASS_HPP #define MY_CPP_CLASS_HPP  class MyCppClass { public:     MyCppClass();     ~MyCppClass();     void doSomething(); };  #endif // MY_CPP_CLASS_HPP  

C++ обертка (wrapper.cpp):

#include "my_cpp_class.hpp" #include <exception>  extern "C" {     // фабрика создания экземпляра класса.     MyCppClass* my_cpp_class_new() {         try {             return new MyCppClass();         } catch (const std::exception& e) {             // можно добавить логирование ошибки.             return nullptr;         }     }      // вызов метода doSomething.     void my_cpp_class_do_something(MyCppClass* instance) {         if (instance) {             try {                 instance->doSomething();             } catch (...) {                 // если нужно, обработка исключений.             }         }     }      // удаление экземпляра.     void my_cpp_class_delete(MyCppClass* instance) {         delete instance;     } }

Rust-обертка для работы с C++:

use std::ptr;  /// представляем opaque тип для C++ класса. #[repr(C)] pub struct MyCppClass {     _private: [u8; 0], }  extern "C" {     fn my_cpp_class_new() -> *mut MyCppClass;     fn my_cpp_class_do_something(instance: *mut MyCppClass);     fn my_cpp_class_delete(instance: *mut MyCppClass); }  /// обертка вокруг C++ класса, инкапсулирующая unsafe-вызовы. pub struct CppClassWrapper {     ptr: *mut MyCppClass, }  impl CppClassWrapper {     /// создает экземпляр класса. Паникует, если создание не удалось.     pub fn new() -> Self {         let ptr = unsafe { my_cpp_class_new() };         if ptr.is_null() {             panic!("Не удалось создать объект MyCppClass");         }         Self { ptr }     }      /// вызывает метод doSomething.     pub fn do_something(&self) {         unsafe { my_cpp_class_do_something(self.ptr) }     } }  impl Drop for CppClassWrapper {     fn drop(&mut self) {         if !self.ptr.is_null() {             unsafe { my_cpp_class_delete(self.ptr) }         }     } }

Минимизировали unsafe-блоки, добавив проверки на NULL и обернув вызовы в Rust API.

PhantomData и обработка ошибок

Пример с использованием RAII уже знаком – автоматическое освобождение ресурсов через трейт Drop. Для более сложных сценариев можно подключить PhantomData для контроля времени жизни:

use std::marker::PhantomData; use std::os::raw::c_void;  /// обертка для ресурса с управлением времени жизни. pub struct SafeHandle<'a> {     handle: *mut c_void,     _marker: PhantomData<&'a ()>, }  impl<'a> SafeHandle<'a> {     /// создает новую обертку, если handle не равен NULL.     pub fn new(handle: *mut c_void) -> Option<Self> {         if handle.is_null() {             None         } else {             Some(Self { handle, _marker: PhantomData })         }     }      /// пример метода, использующего handle.     pub fn do_something(&self) {         unsafe {             // вызов внешней функции с использованием handle.         }     } }  impl<'a> Drop for SafeHandle<'a> {     fn drop(&mut self) {         if !self.handle.is_null() {             unsafe {                 // например, вызов функции освобождения handle.                 // free_handle(self.handle);             }         }     } }

Паниковать – не всегда хорошее решение. Можно использовать типы Result и Option, а также кастомные ошибки для обработки нештатных ситуаций:

use thiserror::Error;  #[derive(Debug, Error)] pub enum FfiError {     #[error("Ошибка создания ресурса: неверный формат строки")]     InvalidCString,     #[error("Ошибка создания ресурса: функция вернула NULL")]     NullResource, }  pub fn create_resource_safe(initial: &str) -> Result<ResourceWrapper, FfiError> {     let c_str = CString::new(initial).map_err(|_| FfiError::InvalidCString)?;     let res_ptr = unsafe { create_resource(c_str.as_ptr()) };     if res_ptr.is_null() {         Err(FfiError::NullResource)     } else {         Ok(ResourceWrapper { ptr: res_ptr })     } }

Этот подход помогает создавать более надежные и поддерживаемые API.

Автоматизация биндингов с помощью bindgen

Bindgen – отличный инструмент для автоматической генерации Rust-оберток по C/C++ заголовочным файлам.

Создадим файл build.rs для автоматической генерации биндингов:

// build.rs extern crate bindgen; use std::env; use std::path::PathBuf;  fn main() {     // указываем путь к заголовочному файлу.     let header_path = "wrapper.h";      // генерируем биндинги с необходимыми опциями.     let bindings = bindgen::Builder::default()         .header(header_path)         .clang_arg("-I./include")  // если заголовки лежат в отдельной папке.         .derive_default(true)         .generate()         .expect("Не удалось сгенерировать биндинги");      // записываем сгенерированные биндинги в OUT_DIR.     let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());     bindings         .write_to_file(out_path.join("bindings.rs"))         .expect("Не удалось записать биндинги"); } В Cargo.toml добавляем: [build-dependencies] bindgen = "0.65.1" Предположим, заголовочный файл wrapper.h выглядит так: // wrapper.h #ifndef WRAPPER_H #define WRAPPER_H  int multiply(int a, int b);  #endif // WRAPPER_H В main.rs используем биндинги: include!(concat!(env!("OUT_DIR"), "/bindings.rs"));  fn main() {     let result = unsafe { multiply(6, 7) };     println!("6 * 7 = {}", result); }

Bindgen позволяет не тратить время на ручное создание оберток, однако всегда проверяйте генерируемый код на соответствие требованиям безопасности и качества.

Потокобезопасность

Работая с внешними библиотеками, нужно учитывать, что они могут не быть потокобезопасными. Для безопасного доступа к таким API следует использовать синхронизацию.

Если библиотека не потокобезопасна, оборачивайте вызовы в мьютексы или другие механизмы синхронизации.

Пример:

use std::sync::{Mutex, Arc};  lazy_static::lazy_static! {     // Глобальный мьютекс для синхронизации вызовов к небезопасному API.     static ref FFI_MUTEX: Mutex<()> = Mutex::new(()); }  /// Функция для синхронизированного вызова unsafe-функции. pub fn synchronized_call<F, R>(f: F) -> R where     F: FnOnce() -> R, {     let _guard = FFI_MUTEX.lock().unwrap();     f() }  fn main() {     let result = synchronized_call(|| unsafe { add(10, 20) });     println!("Синхронизированный вызов: 10 + 20 = {}", result); } Для совместного использования ресурсов между потоками используйте Arc и комбинируйте его с RAII: use std::sync::Arc;  pub struct SharedResource {     inner: Arc<ResourceWrapper>, }  impl SharedResource {     pub fn new(initial: &str) -> Result<Self, FfiError> {         ResourceWrapper::new(initial).map(|res| Self { inner: Arc::new(res) })     }      pub fn use_resource(&self) {         self.inner.use_it();     } }

Так можно безопасно делиться ресурсами, не теряя контроля над их временем жизни.

Интеграция с C-библиотекой обработки изображений

В качестве примера рассмотрим, как можно интегрировать C-библиотеку для обработки изображений в Rust-приложение. Условно есть проверенная временем библиотека на C, которая умеет загружать изображение из файла, применять к нему фильтр и освобождать выделенные ресурсы.

Вместо того чтобы переписывать все это на Rust, создадим надежную и безопасную обертку, которая превращает сырой код в решение с RAII, обработкой ошибок и минимизации unsafe-блоков.

C-часть: библиотека для обработки изображений

/* image_lib.c - простая C-библиотека для загрузки и обработки изображений */ #include <stdio.h> #include <stdlib.h> #include <string.h>  // Определяем структуру, представляющую изображение. typedef struct {     unsigned char *data;    // указатель на пиксельные данные     int width;              // ширина изображения     int height;             // высота изображения     char error_msg[256];    // строка для сообщения об ошибке } Image;  /// Загружает изображение из указанного файла. /// В случае успеха возвращает указатель на Image, в противном - NULL и устанавливает error_msg. /// Для простоты пример не использует реальное декодирование, а просто симулирует загрузку. Image* load_image(const char* file_path) {     if (file_path == NULL || strlen(file_path) == 0) {         return NULL;     }          // Аллоцируем память для структуры Image     Image* img = (Image*)malloc(sizeof(Image));     if (!img) {         return NULL;     }          // Для примера зададим фиксированные размеры     img->width = 800;     img->height = 600;     int size = img->width * img->height * 3; // RGB          // Выделяем память для данных изображения     img->data = (unsigned char*)malloc(size);     if (!img->data) {         free(img);         return NULL;     }          // Симулируем заполнение данными (например, заполняем серым цветом)     memset(img->data, 128, size);          // Очищаем сообщение об ошибке     img->error_msg[0] = '\0';          return img; }  /// Применяет фильтр к изображению. /// filter_type: 0 - инверсия, 1 - градация серого. /// Возвращает 0 при успехе, или отрицательное значение при ошибке. int apply_filter(Image* img, int filter_type) {     if (!img || !img->data) {         return -1; // ошибка: передан некорректный указатель     }          int size = img->width * img->height * 3;     if (filter_type == 0) {         // Простой эффект инверсии цвета         for (int i = 0; i < size; i++) {             img->data[i] = 255 - img->data[i];         }     } else if (filter_type == 1) {         // Эффект градации серого: берем среднее значение для каждого пикселя         for (int i = 0; i < size; i += 3) {             unsigned char gray = (img->data[i] + img->data[i+1] + img->data[i+2]) / 3;             img->data[i] = img->data[i+1] = img->data[i+2] = gray;         }     } else {         snprintf(img->error_msg, sizeof(img->error_msg), "Unknown filter type: %d", filter_type);         return -2; // ошибка: неизвестный тип фильтра     }          return 0; // успех }  /// Освобождает ресурсы, выделенные для изображения. void free_image(Image* img) {     if (img) {         if (img->data) {             free(img->data);         }         free(img);     } }

Rust-часть: обертка для C-библиотеки

//! image_wrapper.rs - безопасная обертка для C-библиотеки обработки изображений. //! //! Здесь мы минимизируем unsafe-блоки, оборачивая C-функции в продакшен-безопасный API, //! который включает RAII для автоматического освобождения ресурсов и грамотную обработку ошибок.  use std::ffi::{CString, CStr}; use std::os::raw::{c_char, c_int}; use std::ptr; use std::error::Error; use std::fmt;  /// Представление C-структуры Image в виде opaque-типа. #[repr(C)] pub struct Image {     _private: [u8; 0], }  /// Объявляем внешние C-функции. extern "C" {     fn load_image(file_path: *const c_char) -> *mut Image;     fn apply_filter(img: *mut Image, filter_type: c_int) -> c_int;     fn free_image(img: *mut Image); }  /// Кастомная ошибка для обработки нештатных ситуаций при работе с C-библиотекой. #[derive(Debug)] pub enum ImageError {     LoadError(String),     FilterError(String),     NullPointer, }  impl fmt::Display for ImageError {     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {         match self {             ImageError::LoadError(msg) => write!(f, "Load image error: {}", msg),             ImageError::FilterError(msg) => write!(f, "Apply filter error: {}", msg),             ImageError::NullPointer => write!(f, "Received null pointer from C function"),         }     } }  impl Error for ImageError {}  /// Безопасная обертка над C-структурой Image. pub struct ImageWrapper {     ptr: *mut Image, }  impl ImageWrapper {     /// Загружает изображение по указанному пути.     /// При ошибке возвращает ImageError.     pub fn new(file_path: &str) -> Result<Self, ImageError> {         // Преобразуем путь к изображению в CString.         let c_path = CString::new(file_path).map_err(|e| ImageError::LoadError(e.to_string()))?;         // Вызов функции загрузки изображения из C.         let img_ptr = unsafe { load_image(c_path.as_ptr()) };         if img_ptr.is_null() {             return Err(ImageError::NullPointer);         }         Ok(ImageWrapper { ptr: img_ptr })     }          /// Применяет фильтр к изображению.     /// filter_type: 0 - инверсия, 1 - градация серого.     pub fn apply_filter(&mut self, filter_type: i32) -> Result<(), ImageError> {         let res = unsafe { apply_filter(self.ptr, filter_type as c_int) };         if res != 0 {             // Если функция вернула ошибку, пытаемся извлечь сообщение об ошибке             // Для простоты, здесь возвращаем фиксированное сообщение.             return Err(ImageError::FilterError(format!("Filter type {} not supported or failed", filter_type)));         }         Ok(())     }          /// Пример метода для извлечения дополнительной информации из C-структуры.     /// В реальной библиотеке может быть функция, возвращающая сообщение об ошибке.     pub fn get_error_message(&self) -> Option<String> {         // Допустим, у нас есть указатель на строку с ошибкой внутри структуры.         // Здесь для примера возвращаем None.         None     } }  impl Drop for ImageWrapper {     fn drop(&mut self) {         // Автоматически освобождаем ресурсы при выходе объекта из области видимости.         if !self.ptr.is_null() {             unsafe { free_image(self.ptr) }         }     } }  #[cfg(test)] mod tests {     use super::*;          #[test]     fn test_image_loading_and_filter() {         // Пробуем загрузить изображение из файла.         let mut img = ImageWrapper::new("example.jpg")             .expect("Failed to load image");                  // Применяем фильтр инверсии.         img.apply_filter(0).expect("Failed to apply inversion filter");                  // Применяем фильтр градации серого.         img.apply_filter(1).expect("Failed to apply grayscale filter");                  // Объект ImageWrapper будет автоматически освобожден.     } }  fn main() {     // Демонстрация использования безопасного API для обработки изображений.     match ImageWrapper::new("sample_image.jpg") {         Ok(mut image) => {             println!("Изображение успешно загружено!");             // Применяем фильтр инверсии.             if let Err(e) = image.apply_filter(0) {                 eprintln!("Ошибка при применении фильтра: {}", e);             } else {                 println!("Фильтр успешно применен!");             }         },         Err(e) => eprintln!("Ошибка загрузки изображения: {}", e),     } }

Создаем простую библиотеку, которая «загружает» изображение (симулируется фиксированный размер и данные), применяет один из двух фильтров и освобождает ресурсы. Функция load_image возвращает указатель на структуру, а apply_filter проверяет корректность входных данных и возвращает код ошибки, если что-то пошло не так.

В Rust уже объявляем внешний API с помощью extern «C», а затем создаем структуру ImageWrapper, которая оборачивает указатель на C-структуру. Конструктор new преобразует строку пути в CString и вызывает C-функцию. Метод apply_filter выполняет вызов в unsafe-блоке и проверяет результат, возвращая Result. RAII реализован через Drop, который гарантирует, что функция free_image будет вызвана при выходе из области видимости.


Заключение

Мы прошли путь от простейших вызовов C-функций до интеграции с полноценными C++ классами, затронув техники управления памятью, синхронизации и автоматизации биндингов. Но, поверьте, это лишь вершина айсберга FFI – о нeм можно рассказывать вечно.

Главная идея проста: не ленитесь изолировать unsafe-код, тестировать всё вдоль и поперек и внимательно изучать документацию.

А как вы используете FFI в своих проектах? Делитесь опытом в комментариях.


ссылка на оригинал статьи https://habr.com/ru/articles/892926/