Здесь для ясности описывается простой пример интеграции функций 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:
На Windows это можно сделать командой:
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. |