yandex-writeups/4.2/1changes.md
2025-12-14 09:49:55 +08:00

475 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

ОР - Умеет собирать статические и динамические библиотеки из Rust кода, и экспортировать функции из них. А также проверять экспортируемые символы с помощью системных утилит (например, nm).
ОР - Умеет описывать Си ABI в Rust коде. (с точки зрения best practice)
ОР - Умеет линковать Си библиотеки к Rust коду (статически, динамически и в рантайме - все три варианта)
ОР - Понимает, как использовать Rust библиотеку в других языках (например, в python) и понимает, для чего это может быть полезно.
ОР - Умеет использовать bindgen и cc для генерации Rust API из Си header файлов и сборки Си библиотеки (https://github.com/DaveGamble/cJSON).
- [x] Умеет собирать статические и динамические библиотеки из Rust кода, и экспортировать функции из них. А также проверять экспортируемые символы с помощью системных утилит (например, nm).
- [x] Умеет описывать Си ABI в Rust коде. (с точки зрения best practice)
- [x] Умеет линковать Си библиотеки к Rust коду (статически, динамически и в рантайме - все три варианта)
- [x] Понимает, как использовать Rust библиотеку в других языках (например, в python) и понимает, для чего это может быть полезно.
- [x] Умеет использовать bindgen и cc для генерации Rust API из Си header файлов и сборки Си библиотеки (https://github.com/DaveGamble/cJSON).
- [x] Умеет импользовать bindgen и cc для генерации Rust API из Си header файлов
- [x] Практика: сборка Си библиотеки (https://github.com/DaveGamble/cJSON).
- [x] Прописать ОРы для глав ✅ 2025-11-17
- [x] Придумать квизы ✅ 2025-11-17
старт
- [ ] Сначала практически расписать бест практис с с аби,
- [x] потом рассказать про биндген
- [x] потом сс
- [x] потом линковка
- [x] ??а если мы захотим уже раст использовать как бблиотеку в другом языке?
- [x] рассказать низкоуровнево как прописывать экспорт раст кода
- [x] (упомянуть про как посмотреть экспорт)
- [x] в том числе и про разные способы линковки
- [x] потом показать на примере линковки к питону
- [ ] потом рассказать про бест практис (возможно решив какую-нибудь проблему в предыдущей главе)
## C ABI Best practice
- [x] Рассказать немного про бест практис, протом перейти к bindgen
- [x] best practice:
- [x] use std::ffi types
- [x] use \*-sys
- [x] Добавление поля links в Cargo.toml крейта \*-sys
- [x] use bindgen
- [x] генерация с build.rs (build dependencies)
- [ ] Option nonnull for c abi (null optimization)
?cstr
## Линковка
Рассказать про то, как низкоуровнево линкуется при помощи rustc, потом рассказать про то, как линковать с build.rs и потом cc
# Start
В прошлом уроке вы узнали, как описывается C ABI в Rust. Давайте теперь посмотрим, как используется C ABI в production коде. Для этого изучим best practice использования C ABI.
## Правильное использование C ABI
ОР - Умеет описывать Си ABI в Rust коде. (с точки зрения best practice)
### Совместимость начинается со стандартов
Допустим, у нас есть такая волшебная функция на С:
```c
int square(int x) {
return x * x;
}
```
Как описать эту функцию в Rust? Возможно так?
```rust
unsafe extern "C" {
fn square(x: i32) -> i32;
}
```
Но тут возникает проблема: в rust явно указана размерность в 32 бита, в то время как в C, int может быть не только размером 32 бита (к примеру у avr, той самой, которая используется в Arduino, размер int 16 бит). Для таких исключений есть модуль core::ffi (который реэкспортирован как std::ffi, так что его можно встретить с импортом по этому пути, но рекомендуется использовать именно по пути core от для поддержки {{no_std}}[https://docs.rust-embedded.org/book/intro/no-std.html] среды). И для описания функций рекомендуется использовать именно эти типы, так как в rust может добавиться поддержка новой архитектуры, со своими типами из C, и программа может незаметно стать памяти-небезопасной.
Перепишем функцию:
```rust
use core::ffi::c_int;
unsafe extern "C" {
fn square(x: c_int) -> c_int;
}
```
Заметка (extern без unsafe):
(может быть под тогл/скрываемый блок?)
Если читать код библиотек, можно встретить, что extern пишется без unsafe (взаимодействие и смысл этого ключевого слова вы изучите в следующем уроке). Так можно было делать раньше, но в rust это стало обязательным с версии 1.85. Идея заключается в том, что пишущий код человек может задекларировать функцию неправильно, и это обязанность программиста, а не компилятора соблюсти здесь безопасность по памяти (пример такой ситуации вы только что разобрали).
### Optional pointer (null optimization) - нужно ли?
### Отделение взаимодействия с ffi в отдельный крейт
Если вы пишете библиотеку, взаимодействующую с C ABI, отделяйте это взаимодействие в отдельный крейт (который обычно называют \*-sys). На это есть несколько веских {{причин}}[https://doc.rust-lang.org/cargo/reference/build-scripts.html#-sys-packages]:
- Несколько разных библиотек могут переиспользовать уже написанный код для взаимод
- ействия с библиотекой
- Так, одна и та же библиотека не будет собираться несколько раз и не будет слинкована несколько раз (то есть не будет существовать несколько разных или одинаковых версий библиотеки в бинаре)
- Легкость изменения библиотеки, от которой зависит бинарь (версии, поиска ее пути и всей остальной конфигурации)
Квиз - множественный выбор
Зачем отделять FFI-взаимодействие в отдельный \*-sys крейт?
Чтобы ускорить выполнение сборки программы с нуля.
Нет. Это не ускорит сборку конченой программы. Но, будет полезно, если разрабатываете библиотеку, ведь при каждом изменении не будет выполнятся build скрипт с линковкой.
Чтобы ускорить выполнение программы во время рантайма.
Нет, отделение логики не ускорит программу.
Чтобы переиспользовать взаимодействие с библиотекой.
Правильно, при отделении логики в -sys крейт, ее смогут переиспользовать другие крейты. Так же, как и при отделении логики в фукнцию, которую можно использовать в другом месте кода
Чтобы не собирать статическую библиотеку несколько раз.
Правильно, при переиспользовании -sys крейта статические библиотеки, которые собираются из исходного кода, будут собраны и включены в бинарь только один раз, вместо сборки в каждом использующем библиотеку крейте
Чтобы было проще обновлять или перенастраивать зависимую C-библиотеку (версия, путь, параметры сборки).
Правильно, программист сможет изменить используемую библиотеку через аттрибут {{target.\<triple>.\<links>}}[https://doc.rust-lang.org/cargo/reference/config.html#targettriplelinks] в файле настроек cargo (./cargo/config.toml)
Чтобы избежать необходимости писать unsafe-код в основном проекте.
Нет. Хоть и часть unsafe логики отделяется в этот крейт (а именно декларация функций), взаимодействие с этими функциями все еще будет не безопасным
### Добавление поля links в Cargo.toml крейта \*-sys
При написании крейта, который линкуется с одной библиотекой, используйте {{links}}[https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key] в манифесте Cargo.toml, для указания, с какой библиотекой происходит линковка:
```toml
# Cargo.toml
[package]
links = "mylib"
```
Благодаря этому, программисты, в коде которых будет использоваться такой крейт, смогут {{перезаписать использование build скрипта}}[https://doc.rust-lang.org/cargo/reference/build-scripts.html#overriding-build-scripts], благодаря чему мы и получим третий плюс из предыдущего пункта
### Автоматическая генерация extern внешних функций
ОР - Умеет использовать bindgen и cc для генерации Rust API из Си header файлов и сборки Си библиотеки (https://github.com/DaveGamble/cJSON).
С обновлением версий библиотек, меняется и их интерфейс. К примеру, могут добавиться новые функции. Но поддерживать актуальные декларации внешних функций (так называемые {{байндинги}}[на английском термин binding, исходит от to bind, который переводится как связывать. говорит компилятору языка, что у нас существует такая функция, а линкеру объясняет, с какой функцией надо связывать]) вручную весьма трудозатратная задача (как и в принципе изначальное написание таких байндингов вручную). На помощь приходит автоматическая генерация при помощи крейта {{bindgen}}[https://github.com/rust-lang/rust-bindgen].
Одним из вариантов использования bindgen является его cli. Установите его:
```bash
cargo install bindgen-cli
```
Создайте новый проект, запишите следующее в файлы:
```rust
// src/main.rs
include!("./bindgen.rs");
fn main() {
println!("Squared eleven: {}", unsafe { square(11) });
}
```
```c
// src/mylib.c
int square(int x) {
return x * x;
}
```
```c
// src/mylib.h
int square(int x);
```
теперь используем bindgen для генерации байндингов:
```bash
bindgen src/mylib.h -o src/bindgen.rs
```
можете посмотреть в файл `src/bindgen.rs`, и увидеть там аналогичную декларацию функции, что вы писали, но сгенерированную автоматически :)
Теперь соберем C библиотеку:
```bash
clang -c src/mylib.c -o mylib.o
```
*Вместо clang можно использовать и gcc (и, теоретически, любой другой C компилятор), но так как rust зависит от инфраструктуры llvm, все примеры будут именно для clang*
И соберем и запустим программу:
```bash
RUSTFLAGS="-L. -l./mylib.o" cargo run
```
*Переменная среды RUSTFLAGS позволяет добавить флаги, которые будут переданы rustc. В данном случае переданы флаги, которые говорят включить в сборку вашу библиотеку. То, как обычно это делается (через скрипт сборки и через cc) будет разобрано чуть позже в этом уроке.*
Ваша программа успешно запустилась и выдала ожидаемый результат:
```
Squared eleven: 121
```
## Автоматизация сборки
ОР - Умеет линковать Си библиотеки к Rust коду (статически, динамически и в рантайме - все три варианта)
Для того, чтобы не делать все это вручную, в cargo есть возможность написать свой скрипт сборщика, который выполнит нужные нам действия перед компиляцией и передаст cargo инструкции для компиляции кода. Обычно такой код расположен в файле build.rs (но путь к нему можно изменить через атрибут {{build}}[https://doc.rust-lang.org/cargo/reference/manifest.html#the-build-field] в Cargo.toml).
Давайте сделаем то же самое, но используя build.rs. Для начала, подчистим выхлоп предыдущего эксперимента:
```bash
rm src/bindgen.rs # но пока оставим mylib.o
```
Теперь напишем build.rs (в корне проекта):
```rust
// build.rs
use bindgen;
use std::{env, path::PathBuf};
fn main() {
let bindings = bindgen::builder()
// Файл, для которого создаются байндинги
.header("src/mylib.h")
// Перезапуск сборки при изменении переданных файлов
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
// Сгенерировать байндинги
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
// Записать получившиеся байндинги в файл OUT_DIR/bindgen.rs
.write_to_file(out_path.join("bindgen.rs"))
.expect("Couldn't write bindings!");
// Передать линкеру, что библиотеки нужно искать в нынешней папке
// Аналогичен флагу -L.
// Информация в cargo передается через stdout
println!("cargo::rustc-link-search=.");
// Передать линкеру, что нужно слинковать с библиотекой ./mylib.o
// Аналогичен флагу -l./mylib.o
println!("cargo::rustc-link-lib=./mylib.o");
}
```
Как вы тут заметили, вместо вызова cli тут используется вызов крейта bindgen. Это рекомендуемый способ использования bindgen, и cli на практике используется редко. Чтобы иметь возможность использовать его в сборочном скрипте, добавьте его в зависимости для сборки:
```bash
cargo add bindgen --build
```
Так же, вместо выхлопа bindgen.rs в папку src/ выходной файл будет находиться внутри папки {{OUT_DIR}}[https://doc.rust-lang.org/cargo/reference/environment-variables.html#:~:text=.exe.-,OUT_DIR,-%E2%80%94%20If%20the%20package]. Это именно та папка, куда build.rs должен ложить все свои выходные файлы. Ни в коем случае не надо ложить файлы в src! Так как выходные файлы не являются сами по себе частью проекта, и являются промежуточным результатом компиляции, то и находится они должны в одной из подпапок target, а именно в OUT_DIR.
Так как путь к bindgen.rs изменился, замените предыдущий include! в src/main.rs на такой:
```rust
include!(concat!(env!("OUT_DIR"), "/bindgen.rs"));
```
Попробуем запустить все это:
```bash
cargo run
```
Байндинги автоматически были созданы и линковка тоже произошла, осталось добавить сборку C кода в библиотеку.
## Сброка C кода в Rust
ОР - Умеет использовать bindgen и cc для генерации Rust API из Си header файлов и сборки Си библиотеки (https://github.com/DaveGamble/cJSON).
Так же, как и для генерации байндингов существует bindgen, для компиляции библиотек существует крейт`cc`. Фактически, он под капотом вызывает компилятор C со всеми задаными флагами. Добавим его в зависимости для сборки:
```bash
cargo add cc --build
```
И обновим build.rs:
```rust
// build.rs
use bindgen;
use cc;
use std::{env, path::PathBuf};
fn main() {
let bindings = bindgen::builder()
// Файл, для которого создаются байндинги
.header("src/mylib.h")
// Перезапуск сборки при изменении переданных файлов
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
// Сгенерировать байндинги
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
// Записать получившиеся байндинги в файл OUT_DIR/bindgen.rs
.write_to_file(out_path.join("bindgen.rs"))
.expect("Couldn't write bindings!");
cc::Build::new()
// добавить src/mylib.c в выходную библиотеку
.file("src/mylib.c")
// скомпилировать C код как библиотеку libmylib.a в папке OUT_DIR
.compile("mylib");
}
```
Заметьте, что пропало явное обозначение, что нужно слинковать с библиотекой. cc сам передаст cargo название библиотеки, с которой нужно слинковать.
Перед запуском, для чистоты эксперимента, удалите старый файл:
```bash
rm mylib.o
```
Теперь сборку и запуск можно делать просто через `cargo run`, не выполняя лишних команд.
## Линковка с динамической библиотекой
До этого мы собирали только со статической библиотекой. Теперь, давайте слинкуем с динамической библиотекой. Для этого, сначала соберем ее:
```bash
clang src/mylib.c -shared -o libmylib.so
```
А теперь, в build.rs уберите использование cc и добавьте в конце функции main:
```rust
fn main() {
// Генерация байндингов
// ...
println!("cargo:rustc-link-search=.");
println!("cargo:rustc-link-lib=dylib=mylib");
}
```
В cargo:rustc-link-lib передано значение dylib=mylib. Оно позволяет явно указать, что нужно подгрузить динамическую библиотеку (dylib) с название mylib (у такой библиотеки будет название libmylib.so на примере linux). Значение \[тип=] опционально и может быть одним из:
- dylib - динамическая библиотека
- static - статическая библиотека
- framework - специфичный формат для MacOS, содержащий динамичекую библиотеку с дополнительными ресурсами
cc не поддерживает генерацию динамических библиотек, так как она не будет являться частью создаваемого cargo и rustc бинаря. Динамические библиотеки обычно являются частью системы (те же libc, zlib, openssl, присутствие которых бинари будут ожидать по стандартным путям). Установлены они могут быть, к примеру, через apt, pacman, установщики windows, и так далее. Либо создаются отдельно и поставляются с программой.
## Линковка в рантайме
ОР - Умеет линковать Си библиотеки к Rust коду (статически, динамически и в рантайме - все три варианта)
Последний способ линковки в рантайме, позволяет не тратить лишний раз оперативную память, загружая код лишь только тогда, когда он нужен. Для независимости от платформы будем использовать dlopen и dlsym.
- dlopen - подгружает нужную нам библиотеку
- dlsym - ищет в библиотеке нужный символ и выдает его адрес, и это может быть не только функция, но и, к примеру, константа, статическая переменая, и тд.
Для начала, удалите build.rs. Добавьте libc как зависимость.
Напишите в src/main.rs:
```rust
// src/main.rs
use libc::{dlopen, dlsym};
use std::ffi::c_int;
fn main() {
let mylib = unsafe { dlopen(c"./libmylib.so".as_ptr(), 0) };
let square: extern "C" fn(c_int) -> c_int =
unsafe { std::mem::transmute(dlsym(mylib, c"square".as_ptr())) };
println!("Squared eleven: {}", unsafe { square(11) });
}
```
Запустив это, получите ровно тот же результат, что и раньше, но теперь библиотеку мы подгружаем сами. Кстати, линкер для динамических библиотек делает то же самое, но подгружает библиотеки еще до вызова main, а адреса функций кладет в заранее заготовленную таблицу функций.
## Практика
Создайте новый проект, с таким содержимым в src/main.rs:
```rust
include!(concat!(env!("OUT_DIR"), "/bindgen.rs"));
use std::ffi::{CStr, CString};
const TEST_JSON: &CStr = c"{
\"meaning_of_life\": 42
}";
fn main() {
let json: *mut cJSON = unsafe { cJSON_Parse(TEST_JSON.as_ptr()) };
let json_str = unsafe { cJSON_PrintUnformatted(json) };
let json_str = unsafe { CString::from_raw(json_str) };
let json_str = json_str.to_str().unwrap();
assert_eq!(json_str, r#"{"meaning_of_life":42}"#);
let meaning_of_life = unsafe { cJSON_GetObjectItem(json, c"meaning_of_life".as_ptr()) };
let meaning_of_life = unsafe { cJSON_GetNumberValue(meaning_of_life) };
println!("Meaning of life: {}", meaning_of_life);
assert_eq!(meaning_of_life, 42f64);
}
```
Создайте такой build.rs, чтобы этот код заработал. Используйте библиотеку [cJSON](https://github.com/DaveGamble/cJSON), ее можно склонировать прямо в корень проекта.
Подсказки:
Экспорт функций можно сгенерировать от cJSON.h используя bindgen
Чтобы была возможность использовать код из cJSON, соберите библиотеку через cc
Байндинги записываются в bindgen.rs в папке, указанной в переменной среды OUT_DIR
Чек-лист:
Линкуется cJSON как статическая библиотека
Файл bindgen.rs с байндингами находится в папке, указанной в переменной среды OUT_DIR
Тесты проходят
Решение:
```rust
use bindgen;
use cc;
use std::{env, path::PathBuf};
fn main() {
let bindings = bindgen::builder()
.header("cJSON/cJSON.h")
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
bindings
.write_to_file(out_path.join("bindgen.rs"))
.expect("Couldn't write bindings!");
cc::Build::new()
// добавить src/mylib.c в выходную библиотеку
.file("cJSON/cJSON.c")
// скомпилировать C код как библиотеку libmylib.a в папке OUT_DIR
.compile("cJSON");
}
```
## Rust как библиотека
ОР - Умеет собирать статические и динамические библиотеки из Rust кода, и экспортировать функции из них. А также проверять экспортируемые символы с помощью системных утилит (например, nm).
ОР - Понимает, как использовать Rust библиотеку в других языках (например, в python) и понимает, для чего это может быть полезно.
В течении всего урока мы использовали C код из Rust кода. А что если нужно будет использовать rust код из других языков, к примеру, того же C, Go, или Python? И это тоже возможно, через тот же C ABI, но на экспорт. Такая функция декларируется так:
```rust
#[unsafe(no_mangle)]
pub extern "C" fn doublefast(x: u32) -> u32 {
x << 1
}
```
- no_mangle - говорит rust, что название функции не надо преобразовывать
- extern "C" - использовать C ABI
- pub extern - экспорт функции
Создайте новый проект как библиотеку:
```bash
cargo new --lib mylib
```
В Cargo.toml добавьте:
```toml
[lib]
crate-type = ["cdylib"] # cdylib означает, что нужно собрать динамическую библиотеку
```
А в src/lib.rs внесите показанный код:
```rust
use std::ffi::CStr;
pub struct Cased {
cstring: *const c_char,
case: bool, // true for uppercase,
// false for lowercase
}
#[unsafe(no_mangle)]
pub extern unsafe "C" fn count_case_ascii(c: Cased) -> u32 {
let cstring = unsafe { CStr::from_ptr(c.cstring) };
let case = (case as u8) << 5;
let mut counter = 0;
for c in cstring.to_bytes() {
// super naive check for character boundary
if !(((65 <= c) && (c <= 90)) && ((97 <= c) && (c <= 122))) {
continue;
}
if case & c != 0 {
counter += 1;
}
}
counter
}
```
Соберите библиотеку. Выходным файлом получится target/debug/libmylib.so (В зависимости от вашей ОС расширение .so, используемое в linux, может замениться на .dylib в macos или .dll в windows). Можем проверить наличие символа в библиотеке для функции через nm из пакета binutils:
```bash
# nm выведет весь список символов
# grep покажет только doublefast, если он есть
nm target/debug/libmylib.so | grep doublefast
```
Попробуем использовать нашу функцию, к примеру, такой код в Python:
```python
from ctypes import cdll
mylib = cdll.LoadLibrary("target/debug/libmylib.dylib")
print(mylib.doublefast(8))
```
Успешно выведет 16
*Информация о том, какие типы функция принимает и выдает не сохраняется в библиотеке, просто по дефолту python считает, что функция принимает и выдает i32*
Такое применение имеет практическую пользу: аналогичный код на rust выполняется в разы быстрее, чем код на python, поэтому много библиотек для python пишутся на C, а со становления rust популяным многие уже пишутся на rust.
### Сборка статической библиотеки
Что собрать то же самое, но в статическую библиотеку, нужно в `crate-type` изменить `cdylib` на `staticlib` (либо можно оставить и то и то, тогда будут собираться обе версии библиотеки):
```toml
[lib]
crate-type = ["staticlib"]
# или можно сделать такой вариант, тогда соберется
# два файла с разными расширениями
# crate-type = ["cdylib", "staticlib"]
```
Попробуем использовать статическую библиотеку, но теперь пример для C:
```c
// main.c
#include <stdint.h>
#include <stdio.h>
uint32_t doublefast(uint32_t);
int main() {
printf("Double six: %d\n", doublefast(6));
}
```
Соберем C код с использованием нашей библиотеки:
```bash
clang main.c target/debug/libmylib.a -o ./a.exe
```
При запуске ./a.exe будет получен ожидаемый результат.
Для crate-type возможны следующие значения:
- staticlib - сборка статической библиотеки
- dylib - сборка динамической библиотеки, предназначенной для использования в rust коде. Собираться использующий код и dylib должны одной и той же версией компилятора, так как у dylib нет стабильного интерфейса
- cdylib - сборка динамической библиотеки, но предназначеную для использования во всех языках, поэтому отличается от dylib:
- интерфейсы фукнций следуют c abi
- в библиотеку будет включена стандартная rust библиотека (rust-std)
- bin - сборка конечной программ
- lib - соберет библиотеку, вид которой будет выбран компилятором (стандартное значение для крейтов-библиотек)
- rlib - внутренний тип библиотек rust
- proc-macro - крейт, в котором экспортированы proc макросы
Квиз - одиночный выбор
Какое значение нужно указать в crate-type, чтобы использовать ваш rust код как динамическую библитеку в проекте на Go?
lib
Нет, lib соберет библиотеку на выбор компилятора
cdylib
Правильно, cdylib соберет динамическую библиотеку с C ABI, который подходит для Go
dylib
Нет, хоть dylib и соберет динамическую библиотеку, но полноценно подходить она будет только для rust кода
staticlib
Нет, staticlib соберет статическую библиотеку
// update
### cbindgen
Так же, как и для генерации rust байндингов из C есть библитека bindgen, для генерации C/C++/cython байндингов из rust кода есть cbindgen:
```bash
cargo install cbindgen
```
## Итоги
В этом уроке мы на практике разобрали, как взаимодействовать с кодом на C в проектах на rust, best practice этого взаимодействия, а так же узнали, как можно взаимодействовать с кодом на rust из других языков. Вы научились автоматически генерировать декларации внешних функций, а так же собирать статические библиотеки при сборке проекта на rust.
Далее вы узнаете, что означает ключевое слово unsafe, зачем оно нужно при взаимодействии с внешними библиотеками, а так же изучите best practice при написании unsafe кода.