tinycrate

Маленькие библиотеки на Rust. Каждая решает одну задачу, без лишних зависимостей и сюрпризов при обновлении.

use tc_ring::Ring;

let mut last: Ring<u32, 4> = Ring::new();
for n in 1..=6 {
    last.push(n); // старые значения вытесняются
}
assert_eq!(last.to_vec(), [3, 4, 5, 6]);
tc-ring v0.4.1 no_std НЕ КАНТОВАТЬ

Крейты

Пока живут здесь и в моих проектах. Как только API устоится, буду публиковать на crates.io. Нажмите на строку, чтобы увидеть пример.

  • tc-ring 0.4.1 Кольцевой буфер фиксированного размера на const generics. Без аллокаций и без unsafe. no_stdcollections

    Хранит последние N значений, новые вытесняют самые старые. Удобно для скользящих окон, истории событий и простых метрик.

    let mut window: Ring<f32, 8> = Ring::new();
    window.push(21.5);
    let avg = window.iter().sum::<f32>() / window.len() as f32;
  • tc-argv 0.2.0 Крошечный парсер аргументов командной строки. Без макросов и без зависимостей. cli

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

    let mut args = tc_argv::Args::from_env();
    let verbose = args.flag("-v");
    let out: String = args.value("--out")?.unwrap_or_else(|| "out.txt".into());
    let files = args.rest();
  • tc-bitset 0.1.3 Битовое множество на стеке: размер задаётся числом слов u64. no_stdconst generics

    Почему размер в словах, а не в битах, разбираю в заметке про const generics ниже.

    use tc_bitset::BitSet;
    
    let mut seen: BitSet<2> = BitSet::new(); // 128 бит
    seen.insert(3);
    seen.insert(100);
    assert!(seen.contains(100));
    assert_eq!(seen.len(), 2);
  • tc-retry 0.3.0 Повтор async-операций с экспоненциальной задержкой и джиттером. Таймер передаётся снаружи, поэтому подходит любой рантайм. async

    Политика повторов описывается один раз и переиспользуется. Ошибки, которые повторять бессмысленно, отсекаются отдельной функцией.

    let page = tc_retry::Policy::exponential(Duration::from_millis(100))
        .max_attempts(5)
        .jitter(0.2)
        .run(|| fetch_page(url), tokio::time::sleep)
        .await?;
  • tc-units 0.0.7 Единицы измерения на уровне типов: метры с секундами просто не сложатся. черновик

    Эксперимент: API ещё будет меняться, в своих проектах пока не используйте.

    use tc_units::{m, s};
    
    let speed = 120.0 * m / (4.0 * s); // Quantity<MetersPerSecond>
    // let oops = 1.0 * m + 1.0 * s;  // не скомпилируется

Заметки

О том, как устроены крейты, какие решения пришлось принять и что пошло не по плану.

6 минут

Кольцевой буфер без unsafe: чем пришлось заплатить

Большинство кольцевых буферов хранят элементы в MaybeUninit<T>, и читать такую память можно только в unsafe. Мне хотелось обойтись без этого.

Читать дальшеСвернуть

В tc-ring слоты хранятся как [Option<T>; N]: каждый либо пуст, либо содержит значение. Массив создаётся через std::array::from_fn(|_| None), и для этого не нужно, чтобы T был Copy или Default.

Цена такого решения: Option<T> бывает больше, чем T. Для ссылок, Box и NonZero-типов компилятор прячет признак «пусто» в недопустимом значении, и размер не меняется. Для u32 или f64 каждый слот вырастает за счёт выравнивания.

Индексы считаются как (head + i) % N. Раз N известен на этапе компиляции, при степени двойки деление по модулю превращается в обычную битовую маску, так что отдельная оптимизация не понадобилась.

4 минуты

Const generics и битсет: где кончается стабильный Rust

Хотелось писать BitSet<128> и получать массив из двух слов. Оказалось, что в стабильном Rust так пока нельзя.

Читать дальшеСвернуть

Для этого тип хранения пришлось бы записать как [u64; BITS / 64], а выражения с обобщёнными константами в длине массива требуют нестабильной возможности generic_const_exprs. Она доступна только на nightly и далека от стабилизации.

Поэтому параметр у tc-bitset — число слов, а не бит: BitSet<2> хранит 128 бит. Не так красиво, зато работает на stable, а проверка индекса стоит одного сравнения с N * 64.

7 минут

Ускоряю сборку: что реально помогло

Рабочий проект собирался заметно дольше, чем хотелось. Вот что я попробовал, в порядке пользы.

Читать дальшеСвернуть
  • cargo build --timings показывает, какие крейты собираются дольше всего и что блокирует параллельную сборку. Начинать стоит с него.
  • Разделить большой крейт на несколько в одном workspace: тогда при правке пересобирается меньше кода.
  • Уменьшить отладочную информацию в dev-профиле: с ней быстрее и линковка, и сама сборка.
  • Быстрый линкер. С Rust 1.90 на x86_64 Linux по умолчанию уже используется LLD, так что выигрыш от mold стал скромнее, но на других платформах он по-прежнему заметен.
  • sccache в CI, чтобы не пересобирать зависимости с нуля на каждый коммит.
# Cargo.toml
[profile.dev]
debug = "line-tables-only"
3 минуты

MSRV без боли: rust-version и третий резолвер

Минимальную поддерживаемую версию Rust стоит объявлять явно. Тогда пользователи старых компиляторов получат понятную ошибку, а не странный сбой сборки.

Читать дальшеСвернуть

Поле rust-version в Cargo.toml сообщает Cargo, с какой версии крейт собирается. Резолвер зависимостей третьей версии учитывает его и при выборе версий зависимостей старается не брать те, что требуют более нового компилятора. В редакции 2024 этот резолвер включён по умолчанию.

Объявленную версию нужно и проверять: в CI у меня есть отдельная задача, которая собирает и тестирует крейт именно на ней.

[package]
name = "tc-ring"
edition = "2024"
rust-version = "1.85"

Об авторе

Я бэкенд-разработчик и пишу на Rust в свободное время. Здесь собираю маленькие крейты, которые сначала понадобились мне самому, и записываю то, что узнал по ходу.

Сайт статический: без трекеров, cookies и аналитики.

  • Лицензия крейтовMIT или Apache-2.0
  • Минимальная версия Rust1.85
  • Редакция2024
  • Зависимости по умолчаниюнет
  • Новые заметкираз в 3–4 недели