1.92K subscribers
3.52K photos
136 videos
15 files
3.73K links
Блог со звёздочкой.

Много репостов, немножко программирования.

Небольшое прикольное комьюнити: @decltype_chat_ptr_t
Автор: @insert_reference_here
Download Telegram
Ещё пара моментов, которые я упустил в предыдущем посте:
— Вопрос после вызовов макросов с {} теперь валиден (можно писать m!{...}?)
— Unicode 14.0
— Много функций пометили #[must_use]
Vec::leak теперь не реалоцирует (кто-нибудь знал что он так делал??)
Ordering теперь #[repr(i8)]

Ну и ссылки на полные чейнджлоги (ставьте лайк если тоже забыли про их существование):
Rust: https://github.com/rust-lang/rust/blob/master/RELEASES.md#version-1570-2021-12-02
Cargo: https://github.com/rust-lang/cargo/blob/master/CHANGELOG.md#cargo-157-2021-12-02
Clippy: https://github.com/rust-lang/rust-clippy/blob/master/CHANGELOG.md#rust-157
#prog #rust #rustreleasenotes

Вышла версия Rust 1.73.0! Как обычно, тут только выдержки, а полный ченджлог — для компилятора, для cargo и для clippy.

В целом довольно минорный релиз, сильных причин обновлять нету.

▪️Текущее поведение компилятора — считать impl-ы трейтов неперекрывающимися, если попытка их унифицировать приводит к циклу в логике. Теперь на это поведение выдаётся предупреждение, поскольку, возможно, это могут поменять в будущем.

▪️В макросах теперь можно вставлять метапеременные типа block после ключевых слов try и async. Пример кода, который не работал раньше, но работает теперь (результат раскрытия второго макроса, конечно, всё ещё требует активации фичи):


macro_rules! create_async {
($body:block) => {
async $body
};
}

macro_rules! create_try {
($body:block) => {
try $body
};
}

▪️Как я уже писал, компилятор теперь ловит безусловную рекурсию в дропах.

▪️Как и обещано, линт cast_ref_to_mut (на касты из &T в &mut T — в том числе и не напрямую) теперь deny по умолчанию.

▪️Задокументирована текущая (v0) используемая rustc версия манглинга имён.

▪️Строку теперь можно индексировать парами core::ops::Bound

▪️Немного поменяли формат сообщений паник по умолчанию для assert! и assert_eq!/assert_ne!. Примеры:

Код:

fn main() {
let file = "ferris.txt";
panic!("oh no! {file:?} not found!");
}

До:

thread 'main' panicked at 'oh no! "ferris.txt" not found!', src/main.rs:3:5

После:

thread 'main' panicked at src/main.rs:3:5:
oh no! "ferris.txt" not found!

Код:

fn main() {
assert_eq!("🦀", "🐟", "ferris is not a fish");
}

До:

thread 'main' panicked at 'assertion failed: `(left == right)`
left: `"🦀"`,
right: `"🐟"`: ferris is not a fish', src/main.rs:2:5

После:

thread 'main' panicked at src/main.rs:2:5:
assertion `left == right` failed: ferris is not a fish
left: "🦀"
right: "🐟"

По моему, стало более читаемо.

▪️Для LocalKey<Cell<T>> и LocalKey<RefCell<T>> (LocalKey — тип, в который заворачиваются значения в макросе thread_local!) добавили несколько методов для прямой манипуляции с значениями, без использования общего with. Мало того, что это позволяет сделать код нагляднее, так ещё и позволяет в некоторых случаях избежать инициализации thread local переменной значением, которое будет тут же перезаписано. При этом в общности API не теряет, поскольку на практике почти всегда из-за требований внутренней изменяемости значение и так было завёрнуто в Cell или RefCell.

▪️Для примитивных беззнаковых числовых типов доступны методы div_ceil (деление с округлением вверх, наконец-то!), next_multiple_of и checked_next_multiple_of. Все из них работают в cosnt-контексте.

▪️Ещё в const-контексте теперь можно создавать слабые ссылки (и Arc-сорта тоже) и переводить NonNull в ссылку.
👍91
#prog #rust

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

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

При разработке большого проекта избежать дублирования зависимостей сложно. Это не является неизбежностью, поскольку cargo старается по возможности выбирать одну версию библиотеки, которая удовлетворяет всем ограничениям на версию, но всё же это порой случается — особенно, если в зависимостях где-то ограничение на версию сверху. Иногда на это могут быть обоснованные причины — например, когда используются две разные мажорные версии библиотеки, у которых из общего в основном только название — но чаще это просто увеличивает технический долг.

Убирать дубликаты зависимостей не всегда так же просто, как просто вызвать cargo update (говорю по своему опыту), но и оставлять их в долгосрочной перспективе не стоит. Не всегда можно сразу одним разом убрать дубликаты, но даже если и можно, то это часто может стать блокером для внесения других изменений в кодовую базу. С другой стороны, если за этим не следить, новые изменения могут добавить новые дубликаты зависимостей, только увеличивая время компиляции и технический долг.

Один из способов контролировать технический долг — это зафиксировать его состояние и убедиться, что он не растёт. Конкретно в данном случае это решение довольно простое: вынести список дубликатов зависимостей в отдельный файл, который включается в репозиторий, и вставить в CI проверку, которая будет сравнивать этот список с реальным списком дубликатов.

Именно это я недавно сделал у себя на работе. Весь нужный для этого код уместился в чуть менее, чем в 200 строчек кода на Rust — и это включая пустые строки и комментарии. Если бы я меньше заботился о диагностике и некоторых допфичах (об этом ниже), то вышло бы ещё меньше. В связи с этим я хочу поделиться некоторыми практическими советами на тот случай, если вы захотите сделать нечто подобное у себя.

Самый насущный из практических вопросов: куда положить нужный код? Возможно, наиболее идеологически правильным было бы отнести это в отдельную стадию внешнего линтинга, но это значит, что нужный код нужно или иметь в качестве уже собранного бинарника и каким-то образом подтягивать его, или собирать во время сборки. Оба подхода усложняют CI, особенно первый. Всех этих хлопот можно избежать, если включиться в шаг, который уже подразумевает компиляцию кода и его запуск. Именно, нужный код можно поместить в тесты, и он будет автоматически запускаться во время cargo test. В этом случае код для проверки будет запускаться автоматически для каждого нового изменения (вы же запускаете тесты в CI, верно?) и завалит тесты в случае изменения списка дубликатов, достигая нужной нам цели.

Второй по важности момент — как именно получать список дублирующихся зависимостей. Для этого можно использовать cargo, и если вы разрабатываете проект на Rust, почти наверняка вы уже его используете, поэтому это не приносит новых зависимостей для сборки. Нам понадобится cargo tree (команда доступна с версии cargo 1.44) с ключом -d (--duplicates). Задача кажется простой, но тут есть пара тонкостей, о которых стоит упомянуть. ⬇️
👍4🔥21
По умолчанию cargo tree -d показывает не только дубликаты зависимостей, но и зависимые от них крейты. Это удобно для человека, но не особо пригодно для наших целей, когда мы хотим получить только дубликаты и ничего более. Для того, чтобы оставить только нужные нам результаты, можно воспользоваться флагом --depth=0 (доступен с версии cargo 1.54). В этом случае обработка вывода cargo tree будет сведена к минимуму.

А препроцессировать его придётся. Ввиду того, что вывод cargo tree для пользователя, а не для обработки машиной (и потому чисто технически это всё может сломаться с обновлением версии cargo), в нём есть пустые строки (которые отделяют деревья зависимостей друг от друга) и строки, предваряющие разных секции зависимостей, а именно, [build-dependencies] и [dev-dependencies]. Эти строки при обработке вывода нужно убирать.

Кстати, насчёт зависимостей: у cargo tree есть флаг -e/--edges, который указывает, какие именно зависимости показывать. По умолчанию это прямые зависимости, build-зависимости (которые нужны для сборки, но не нужны непосредственно для компиляции исполняемого кода — скажем, генератор кода поверх .proto-файлов) и dev-зависимости (не нужные для сборки, но полезные для разработки — обычно это зависимости для тестов и бенчмарков). Лично меня куда меньше волнует дублирование, вызванное dev-зависимостями, во многом из-за того, что они не компилируются при каждом cargo build/cargo check. Если вы разделяете моё мнение на этот счёт, используйте флаг -e=normal,build.

Ещё один из дефолтов cargo tree — консистентный с остальными командами — по умолчанию код генерируется только для текущей целевой платформы и с фичами проекта по умолчанию. Из-за этого можно упустить дубликаты, которые появляются при компиляции под другие платформы и с другими фичами. Конкретно в моём случае это не является проблемой, поскольку в силу специфики используемых технологий компонент, над которым я работаю, поддерживает только один target triple, а все наши features нужны исключительно для отладки, но, скорее всего, имеет смысл использовать флаги --all-targets и --all-features. Только имейте в виду, что по очевидным причинам это может сильно замедлить скорость проверки.

Теперь немного о самом процессе проверки. Вывод cargo tree выводит имя зависимости вместе с версией. Нам нужно только имя зависимости для исключения дубликатов. К счастью, так как имя пакета не может включать в себя пробел, выделение имени — это просто

let name = line.split_once(' ').unwrap().0;


Именно это значение нам и надо сохранить. Можно также считать разными зависимости несовместимых версий, но это несколько менее удобно в обработке (код не проверял):

let (name, version) = line.split_once(' ').unwrap();
let version = {
let (major, rest) = version.split_once('.').unwrap();
if major == "v0" {
let minor = rest.split_once('.').unwrap().0;
format!("v0.{minor}")
} else {
major.to_owned()
}
};
// (name, version) используется в качестве ключа


Надо отметить, что в этом случае в списке будут не только сами зависимости, но и их версии, что может создать неудобства при их обновлении.

Насчёт обработки списков: сигнализировать об ошибке стоит не только в том случае, если есть дубликат вне зафиксированного списка, но и в том случае, если зависимость вне фиксированного списка отсутствует в выдаче cargo tree. Это позволяет поддерживать список в актуальном, не врущем состоянии. ⬇️
🔥52