Немного C в вашем Rust
Использование кода на C или C++ внутри проекта на Rust состоит из двух основных частей:
- Обертывание открытого API на C для использования в Rust
- Сборка кода на C или C++ для интеграции с кодом на Rust
Поскольку C++ не имеет стабильного ABI для компилятора Rust, рекомендуется использовать ABI C при комбинировании Rust с C или C++.
Определение интерфейса
Перед использованием кода на C или C++ из Rust необходимо определить (на Rust) типы данных и сигнатуры функций, существующие в связанном коде. В C или C++ вы бы подключили заголовочный файл (.h или .hpp), который определяет эти данные. В Rust необходимо либо вручную перевести эти определения в Rust, либо использовать инструмент для их автоматической генерации.
Сначала мы рассмотрим ручной перевод этих определений из C/C++ в Rust.
Обертывание функций и типов данных C
Обычно библиотеки, написанные на C или C++, предоставляют заголовочный файл, определяющий все типы и функции, используемые в публичных интерфейсах. Пример такого файла может выглядеть следующим образом:
/* File: cool.h */
typedef struct CoolStruct {
int x;
int y;
} CoolStruct;
void cool_function(int i, char c, CoolStruct* cs);
При переводе в Rust этот интерфейс будет выглядеть так:
/* File: cool_bindings.rs */
#[repr(C)]
pub struct CoolStruct {
pub x: cty::c_int,
pub y: cty::c_int,
}
extern "C" {
pub fn cool_function(
i: cty::c_int,
c: cty::c_char,
cs: *mut CoolStruct
);
}
Разберем это определение по частям, чтобы объяснить каждый компонент.
#[repr(C)]
pub struct CoolStruct { ... }
По умолчанию Rust не гарантирует порядок, выравнивание или размер данных, включенных в struct. Чтобы обеспечить совместимость с кодом на C, мы используем атрибут #[repr(C)], который указывает компилятору Rust всегда использовать те же правила, что и C, для организации данных внутри структуры.
pub x: cty::c_int,
pub y: cty::c_int,
Из-за гибкости определения int или char в C или C++ рекомендуется использовать типы из модуля cty, такие как c_int и c_char, чтобы обеспечить совместимость с платформой.
extern "C" {
pub fn cool_function(
i: cty::c_int,
c: cty::c_char,
cs: *mut CoolStruct
);
}
Блок extern "C" сообщает компилятору Rust, что указанные функции используют ABI C, обеспечивая их совместимость с функциями, определенными в коде на C. Указатель *mut CoolStruct соответствует указателю CoolStruct* в C, позволяя передавать изменяемые структуры между языками.
Автоматизация с помощью bindgen
Ручной перевод заголовочных файлов может быть трудоемким и подверженным ошибкам, особенно для больших библиотек. Инструмент bindgen автоматизирует этот процесс, генерируя определения Rust из заголовочных файлов C или C++.
Чтобы использовать bindgen, добавьте его в зависимости вашего проекта в Cargo.toml:
[build-dependencies]
bindgen = "0.59"
Затем создайте скрипт build.rs для генерации привязок:
use bindgen;
fn main() {
println!("cargo:rerun-if-changed=wrapper.h");
bindgen::Builder::default()
.header("wrapper.h")
.generate()
.expect("Unable to generate bindings")
.write_to_file("src/bindings.rs")
.expect("Couldn't write bindings!");
}
Файл wrapper.h должен включать заголовочные файлы C, которые вы хотите преобразовать:
/* wrapper.h */
#include "cool.h"
Запуск cargo build сгенерирует файл src/bindings.rs, содержащий определения Rust для всех типов и функций из cool.h. Используйте их в вашем коде на Rust:
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
Сборка кода на C/C++
После определения интерфейса необходимо скомпилировать код на C или C++ и связать его с вашим проектом на Rust. Это обычно делается с помощью скрипта build.rs.
Скрипты сборки
Скрипт build.rs — это файл, написанный на синтаксисе Rust, который выполняется на вашей машине компиляции ПОСЛЕ сборки зависимостей вашего проекта, но ДО сборки самого проекта.
Полное описание можно найти здесь. Скрипты build.rs полезны для генерации кода (например, через [bindgen]), вызова внешних систем сборки, таких как Make, или прямой компиляции C/C++ с использованием крейта cc.
Вызов внешних систем сборки
Для проектов с сложными внешними проектами или системами сборки проще всего использовать std::process::Command для вызова других систем сборки, переходя по относительным путям, вызывая фиксированную команду (например, make library) и затем копируя полученную статическую библиотеку в нужное место в директории сборки target.
Хотя ваш крейт может быть нацелен на платформу no_std, ваш build.rs выполняется только на машинах, компилирующих ваш крейт. Это означает, что вы можете использовать любые крейты Rust, которые работают на вашем хосте компиляции.
Сборка кода на C/C++ с помощью крейта cc
Для проектов с ограниченными зависимостями или сложностью, или для проектов, где трудно модифицировать систему сборки для создания статической библиотеки (вместо финального бинарного файла или исполняемого файла), проще использовать крейт [cc], который предоставляет идиоматичный интерфейс Rust к компилятору, предоставляемому хостом.
В простейшем случае компиляции одного файла C в качестве зависимости для статической библиотеки пример скрипта build.rs, использующего крейт [cc], будет выглядеть так:
fn main() {
cc::Build::new()
.file("src/foo.c")
.compile("foo");
}
Файл build.rs размещается в корне пакета. Затем cargo build скомпилирует и выполнит его перед сборкой пакета. Генерируется статический архив с именем libfoo.a, который помещается в директорию target.