bindgen: как добавить в проект Rust библиотеку на языке C Печать
Добавил(а) microsin   

Здесь для ясности описывается простой пример интеграции функций inc и dec на языке C в код проекта Rust (использовался перевод статьи [1]).

Предположим, что у вас есть C-функции:

int inc(int x) {
return x + 1; }

int dec(int x) {
return x - 1; }

Как эти функции добавить в проект Rust? Мне удалось разобраться, как это можно сделать с помощью утилиты bindgen, компилятора gcc и библиотекаря ar.

Bindgen это программный инструмент, который позволяет конвертировать C-код в модуль Rust. Различают 2 вида bindgen:

1. bindgen как утилита командной строки (на Ubuntu устанавливается командой sudo apt install bindgen). На входе она принимает код на языке C, а на выходе генерирует файл *.rs, который можно использовать как модуль Rust.

2. Крейт bindgen: это библиотека Rust, которая делает то же самое, что и утилита командной строки, но с помощью скрипта build.rs, в котором вы описываете процесс конвертации C-кода в код Rust.

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

[Использование утилиты bindgen]

1. Создайте файлы foo.c и foo.h, и поместите их в корневой каталог проекта Rust (каталог, в котором вы запускаете команды cargo).

Файл foo.h:

#pragma once

int inc(int x);
int dec(int x);

Файл foo.c:

int inc(int x) {
return x + 1; }

int dec(int x) {
return x - 1; }

2. С помощью утилиты bindgen сгенерируйте модуль Rust в каталоге src:

$ bindgen foo.c -o src/bindings.rs

В результате запуска этой команды получится вот такой файл:

/* automatically generated by rust-bindgen 0.66.1 */

extern "C" {
pub fn inc(x: ::std::os::raw::c_int) -> ::std::os::raw::c_int; }

extern "C" {
pub fn dec(x: ::std::os::raw::c_int) -> ::std::os::raw::c_int; }

Добавьте ключевое слово unsafe в декларацию этих функций:

unsafe extern "C" {
pub fn inc(x: ::std::os::raw::c_int) -> ::std::os::raw::c_int; }

unsafe extern "C" {
pub fn dec(x: ::std::os::raw::c_int) -> ::std::os::raw::c_int; }

3. Добавьте в проект bindings.rs, и вызовите функцию dec в модуле src/main.rs:

include!("bindings.rs");

fn main() {
println!("Hello, world!");
unsafe{
println!("{}", dec(5));
} }

4. Создайте в корне проекта файл build.rs со следующим содержанием:

fn main() {
println!("cargo:rerun-if-changed=foo.o");

// Указываем путь для поиска библиотек
println!("cargo:rustc-link-search=native=.");

// Линкуем объектный файл как статическую библиотеку
println!("cargo:rustc-link-lib=static=foo"); }

5. Создайте библиотеку libfoo.a из файла foo.c:

$ gcc -c -o foo.o foo.c
$ ar rcs libfoo.a foo.o

Теперь можно проверить, как интегрировался код foo.c в проект Rust:

$ cargo build
   Compiling myproj v0.1.0 (/home/user/rustprojects/myproj)
...
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
$ cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/myproj`
Hello, world!
4

Как видите, встроенный C-код успешно работает!

[Встраивание static inline функций]

На входе у нас есть файл foo.h, который находится в корне проекта Rust:

static inline int inc(int x) {
return x + 1; }

static int dec(int x) {
return x - 1; }

Далее процесс по шагам (на основе статьи [1]):

1. Сгенерируйте файл src/bindings.rs командой:

$ bindgen --experimental --wrap-static-fns foo.h -o src/bindings.rs

Получится вот такой файл:

/* automatically generated by rust-bindgen 0.66.1 */

extern "C" {
#[link_name = "inc__extern"]
pub fn inc(x: ::std::os::raw::c_int) -> ::std::os::raw::c_int; }

extern "C" {
#[link_name = "dec__extern"]
pub fn dec(x: ::std::os::raw::c_int) -> ::std::os::raw::c_int; }

Нам нужно передать флаг --experimental, потому что эта фича не является полностью доработанной. Тем не менее, есть хорошая новость: теперь мы в файле bindings.rs получили привязку к коду Rust для функций inc и dec.

2. Создайте файл foo.c со следующим содержимым, разместите его в корне проекта, как и файл foo.h:

// Файл foo.c:
#include "foo.h"

int inc__extern(int x) { return inc(x); }
int dec__extern(int x) { return dec(x); }

Эти __extern функции служат обертками для статических функций, которые мы определили в нашем файле foo.h. Теперь нам нужно скомпилировать файл foo.c в библиотеку:

$ clang -O -c -o foo.o foo.c
$ objdump -d foo.o
foo.o: file format elf64-x86-64

Disassembly of section .text:
0000000000000000 < inc__extern>: 0: 8d 47 01 lea 0x1(%rdi),%eax 3: c3 ret 4: 66 66 66 2e 0f 1f 84 data16 data16 cs nopw 0x0(%rax,%rax,1) b: 00 00 00 00 00
0000000000000010 < dec__extern>: 10: 8d 47 ff lea -0x1(%rdi),%eax 13: c3 ret

Как мы видим в результате дизассемблирования, объектный файл foo.o содержит 2 символа: inc__extern и dec__extern. Они заменят inc и dec а наших Rust bindings, и именно поэтому оба объявления функций в привязках имеют атрибут #[link_name], переопределяющий имя для линковки.

3. Превратим наш объектный файл foo.o в статическую библиотеку libfoo.a:

$ ar rcs libfoo.a foo.o

На Windows это можно сделать командой:

> LIB foo.o /OUT:foo.lib

4. Теперь мы можем выполнить линковку наших bindings со статической библиотекой libfoo.a, чтобы её код мог использоваться проектом Rust.

[Автоматизация с помощью build.rs]

Ту же самую процедуру можно выполнить в сценарии сборки (файл build.rs, размещенный в корне проекта). Процесс по шагам:

1. Добавьте в файл Cargo.toml зависимость сборки (секция build-dependencies) для bindgen:

[package]
name = "myproj"
version = "0.1.0"
edition = "2024"

[dependencies]

[build-dependencies]
bindgen = "0.72.1"

2. Создайте в корневом каталоге проекта файл build.rs. Он будет автоматически запускаться при вызове команды cargo build:

use bindgen::builder;
use std::path::PathBuf;
use std::process::Command;

fn main() {
let input = "foo.c";
let output_path = PathBuf::from(std::env::var("OUT_DIR").unwrap());

// Пути относительно OUT_DIR
let obj_path = output_path.join("foo.o");
let lib_path = output_path.join("libfoo.a");

// Укажем bindgen генерировать обертки для static-функций:
let bindings = builder()
.header(input)
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.wrap_static_fns(true)
.generate()
.unwrap();

// Компиляция сгенерированных оберток в объектный файл.
let clang_output = std::process::Command::new("clang")
.arg("-O")
.arg("-c")
.arg("-o")
.arg(&obj_path)
.arg(input)
.output()
.unwrap();

if !clang_output.status.success() {
panic!(
"Could not compile object file:\n{}",
String::from_utf8_lossy(&clang_output.stderr)
);
}

// Превращение объектного файла в статическую библиотеку:
#[cfg(not(target_os = "windows"))]
let lib_output = Command::new("ar")
.arg("rcs")
.arg(&lib_path)
.arg(&obj_path)
.output()
.unwrap();

#[cfg(target_os = "windows")]
let lib_output = Command::new("LIB")
.arg(&obj_path)
.arg(format!("/OUT:{}", lib_path.display()))
.output()
.unwrap();

if !lib_output.status.success() {
panic!(
"Could not emit library file:\n{}",
String::from_utf8_lossy(&lib_output.stderr)
);
}

// Укажем cargo статически линковать библиотеку `libfoo.a`,
// указываем Cargo где искать библиотеку:
println!("cargo:rustc-link-search=native={}", output_path.display());
println!("cargo:rustc-link-lib=static=foo");

// Запись в файл rust bindings:
bindings
.write_to_file(output_path.join("bindings.rs"))
.expect("Cound not write bindings to the Rust file");

// ⭐ ВАЖНО: сообщаем Cargo перезапускать build.rs при изменении foo.c.
// Перезапуск build.rs будет также срабатывать и при модификации foo.h,
// потому что файл foo.c подключает файл foo.h.
println!("cargo:rerun-if-changed={}", input); }

3. Укажите в файле main.rs подключать bindings.rs из временного каталога:

//include!("bindings.rs");
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

fn main() {
println!("Hello, world!");
unsafe{
println!("{}", dec(5));
} }

После этого запустите cargo clean, cargo build и cargo run, как обычно.

[Ссылки]

1. How to handle static inline functions site:github.com.