Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Немного C в вашем Rust

Использование C или C++ внутри проекта на Rust состоит из двух основных частей:

  • оборачивание открытого C API для использования из Rust;
  • сборка вашего C- или C++-кода, чтобы его можно было интегрировать с кодом на Rust.

Поскольку у C++ нет стабильного ABI, на который мог бы ориентироваться компилятор Rust, при объединении Rust с C или C++ рекомендуется использовать ABI C.

Определение интерфейса

Прежде чем использовать код на C или C++ из Rust, нужно определить (на Rust), какие типы данных и сигнатуры функций существуют в подключаемом коде. В C или C++ вы включили бы заголовочный файл (.h или .hpp), в котором определены эти данные. В Rust нужно либо вручную перевести эти определения на Rust, либо использовать инструмент, который их сгенерирует.

Сначала мы рассмотрим ручной перевод этих определений из C/C++ в Rust.

Оборачивание функций и типов данных C

Обычно библиотеки на C или C++ предоставляют заголовочный файл, в котором определены все типы и функции, используемые в открытых интерфейсах. Пример такого файла может выглядеть так:

/* Файл: cool.h */
typedef struct CoolStruct {
    int x;
    int y;
} CoolStruct;

void cool_function(int i, char c, CoolStruct* cs);

При переводе на Rust этот интерфейс будет выглядеть так:

/* Файл: 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 не гарантирует порядок, выравнивание (padding) или размер данных, входящих в struct. Чтобы гарантировать совместимость с C-кодом, мы добавляем атрибут #[repr(C)], который указывает компилятору Rust всегда использовать для организации данных внутри структуры те же правила, что и C.

pub x: cty::c_int,
pub y: cty::c_int,

Из-за гибкости в том, как C или C++ определяют int или char, рекомендуется использовать примитивные типы данных, определённые в cty: они сопоставляют типы C с типами Rust.

extern "C" { pub fn cool_function( ... ); }

Эта инструкция определяет сигнатуру функции, которая использует ABI C и называется cool_function. Поскольку определяется только сигнатура без тела функции, сама функция должна быть предоставлена где-то ещё или связана с итоговой библиотекой или бинарным файлом из статической библиотеки.

    i: cty::c_int,
    c: cty::c_char,
    cs: *mut CoolStruct

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

Здесь есть один новый тип: *mut CoolStruct. Поскольку в C нет понятия ссылок Rust, которые выглядели бы так: &mut CoolStruct, мы вместо этого используем сырой указатель. Поскольку разыменование этого указателя — unsafe, а сам указатель может оказаться нулевым (null), нужно соблюдать осторожность, чтобы сохранить гарантии, типичные для Rust, при взаимодействии с кодом на C или C++.

Автоматическая генерация интерфейса

Вместо того чтобы вручную создавать эти интерфейсы, что может быть утомительно и чревато ошибками, существует инструмент bindgen, который выполнит эти преобразования автоматически. Инструкции по использованию bindgen см. в руководстве пользователя bindgen; обычно процесс состоит из следующих шагов:

  1. Соберите все заголовочные файлы C или C++, которые определяют интерфейсы или типы данных, которые вы хотите использовать из Rust.
  2. Напишите файл bindings.h, который #include "..." каждый из файлов, собранных на первом шаге.
  3. Передайте этот файл bindings.h вместе с любыми флагами компиляции, которые использовались для сборки вашего кода, в bindgen. Совет: используйте Builder.ctypes_prefix("cty") / --ctypes-prefix=cty и Builder.use_core() / --use-core, чтобы сгенерированный код был совместим с #![no_std].
  4. bindgen выведет сгенерированный код Rust в терминал. Этот вывод можно перенаправить в файл в вашем проекте, например bindings.rs. Вы можете использовать этот файл в своём проекте на Rust для взаимодействия с кодом C/C++, скомпилированным и связанным как внешняя библиотека. Совет: не забудьте использовать крейт cty, если типы в сгенерированных привязках имеют префикс cty.

Сборка вашего кода на C/C++

Поскольку компилятор Rust не умеет напрямую компилировать код на C или C++ (как и код на любом другом языке, предоставляющем интерфейс C), код не на Rust нужно скомпилировать заранее.

Для встраиваемых проектов это чаще всего означает компиляцию кода на C/C++ в статический архив (например, cool-library.a), который затем объединяется с кодом на Rust на финальном шаге компоновки.

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

Если ваш код существует как исходный проект, нужно скомпилировать код на C/C++ в статическую библиотеку: либо запустив вашу существующую систему сборки (например, make, CMake и т. д.), либо перенеся необходимые шаги компиляции на инструмент, называемый крейтом cc. Для обоих этих способов нужен скрипт build.rs.

Скрипты сборки build.rs на Rust

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.