Русский
Русский
English
Статистика
Реклама

Release

Перевод Rust 1.45.0 стабилизация функциональных процедурных макросов, исправление дефектов преобразования

18.07.2020 18:23:19 | Автор: admin

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


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.45.0 вам достаточно выполнить следующую команду:


rustup update stable

Если у вас ещё не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта, а также посмотреть на GitHub.


Что вошло в стабильную версию 1.45.0


Данный выпуск содержит два больших изменения: исправление давних дефектов при преобразовании между целыми числами и числами с плавающей точкой и стабилизация фич, необходимых для того, чтобы как минимум один веб-фреймворк смог работать на стабильном Rust.


Исправление дефектов в преобразованиях


Изначально Issue 10184 была открыта в октябре 2013 года, за полтора года до выпуска Rust 1.0. Так как rustc использует LLVM в качестве backend-компилятора, когда вы пишете подобный код:


pub fn cast(x: f32) -> u8 {    x as u8}

компилятор Rust в версиях 1.44.0 и раньше генерировал следующее LLVM-IR:


define i8 @_ZN10playground4cast17h1bdf307357423fcfE(float %x) unnamed_addr #0 {start:  %0 = fptoui float %x to i8  ret i8 %0}

fptoui реализует преобразование и является сокращением от "floating point to unsigned integer".


Но здесь есть проблема, описанная в документации:


Инструкция fptoui преобразовывает операнд с плавающей точкой в ближайшее (округляя до нуля) беззнаковое целое значение. Если значение не помещается в ty2, то результирующее значение будет испорченным.
Оригинал
The fptoui instruction converts its floating-point operand into the nearest (rounding towards zero) unsigned integer value. If the value cannot fit in ty2, the result is a poison value.

Следующая часть, если только вы регулярно не копаетесь в недрах компиляторов, может быть не совсем понятна. Она полна жаргона, но есть более простое объяснение: если вы приводите большое число с плавающей запятой к маленькому целому числу, вы получаете неопределённое поведение.


Это означает что, например, поведение следующего кода не определено:


fn cast(x: f32) -> u8 {    x as u8}fn main() {    let f = 300.0;    let x = cast(f);    println!("x: {}", x);}

На моём компьютере с Rust 1.44.0 этот код печатает "x: 0", но т.к. его поведение не определено, напечатать он может всё что угодно. Это мы называем ошибкой корректности (ведь unsafe кода тут нет) то есть ошибка, когда компилятор делает неправильные вещи. Мы отмечаем их в нашем трекере как I-unsound, и относимся к ним очень серьёзно.


Однако эта ошибка заняла много времени для исправления. Причина в том, что было очень неясно, каким должен быть правильный путь.


В итоге было принято решение сделать так:


  • as будет выполнять "насыщающее приведение" (saturating cast),
  • будет добавлено новое unsafe приведение, если вы хотите пропустить проверки.

Это очень похоже на доступ к массиву, например:


  • array[i] проверит, чтобы убедиться, что array содержит по крайней мере i + 1 элемент,
  • можно использовать unsafe { array.get_unchecked(i) }, чтобы пропустить проверку.

Итак, что такое насыщающее приведение? Давайте посмотрим на слегка изменённый пример:


fn cast(x: f32) -> u8 {    x as u8}fn main() {    let too_big = 300.0;    let too_small = -100.0;    let nan = f32::NAN;    println!("too_big_casted = {}", cast(too_big));    println!("too_small_casted = {}", cast(too_small));    println!("not_a_number_casted = {}", cast(nan));}

Выведет:


too_big_casted = 255too_small_casted = 0not_a_number_casted = 0

То есть слишком большие числа превращаются в максимально возможное значение. Слишком малые числа дают наименьшее возможное значение (равное нулю). NaN выдаёт ноль.


А это новый API для небезопасного приведения:


let x: f32 = 1.0;let y: u8 = unsafe { x.to_int_unchecked() };

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


Стабилизация функциональных процедурных макросов в выражениях, шаблонах и стейтментах


В Rust 1.30.0 мы стабилизировали функциональные процедурные макросы в позиции элемента. Например, крейт gnome-class:


Gnome-класс это процедурный макрос для Rust. Внутри макроса мы определяем Rust-о подобный мини-язык, имеющий расширения, позволяющие вам определять подклассы GObject, их свойства, сигналы, реализации интерфейса и остальные функции GObject. Цель состоит в том, чтобы не требовать небезопасного кода с вашей стороны.

Это выглядит так:


gobject_gen! {    class MyClass: GObject {        foo: Cell<i32>,        bar: RefCell<String>,    }    impl MyClass {        virtual fn my_virtual_method(&self, x: i32) {            ... do something with x ...        }    }}

В "позиции элемента" это некий жаргон, но в основном это означает, что вы можете вызывать только gobject_gen! в определённых местах в вашего кода.


Rust 1.45.0 добавляет возможность вызывать процедурные макросы в трёх новых местах:


// представим, что мы имеем процедурный макрос "mac"mac!(); // позиция элемента, то, что было стабилизировано ранее// но здесь представлены 3 новых:fn main() {  let expr = mac!(); // в выражении  match expr {      mac!() => {} // в шаблоне  }  mac!(); // в стейтменте}

Возможность использовать макросы в большем количестве мест интересна, но есть ещё одна причина, по которой многие разработчики давно ждали эту функцию: Rocket. Популярный веб-фреймворк Rocket, первоначально выпущенный в декабре 2016 года, часто называют одной из лучших вещей, которую может предложить экосистема Rust. Вот пример "Привет, мир" из его предстоящего релиза:


#[macro_use] extern crate rocket;#[get("/<name>/<age>")]fn hello(name: String, age: u8) -> String {    format!("Hello, {} year old named {}!", age, name)}#[launch]fn rocket() -> rocket::Rocket {    rocket::ignite().mount("/hello", routes![hello])}

До этого дня Rocket зависела от функциональности из ночной версии компилятора для предоставления своей гибкости и эргономики. По факту, как можно видеть на домашней странице проекта, тот же пример что выше в текущей версии Rocket требует наличия свойства proc_macro_hygiene для компиляции. Тем не менее, как вы можете догадаться из названия свойства, сегодня оно попадёт стабильный выпуск! Данная проблема для отслеживания истории ночных функций в Rocket. Теперь они все проверены и готовы к использованию!


Следующая версия Rocket всё ещё находится в разработке, но когда она выйдет, многие будут очень довольны :)


Изменения в стандартной библиотеке


В Rust 1.45.0 были стабилизированы следующие функции:



Также теперь можно использовать char с диапазонами для итерации по символам:


for ch in 'a'..='z' {    print!("{}", ch);}println!();// Выведет "abcdefghijklmnopqrstuvwxyz"

Полный список изменений вы можете увидеть в детальных примечаниях к выпуску.


Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.45.0


Множество людей собрались вместе, чтобы создать Rust 1.45.0. Мы не смогли бы сделать это без всех вас, спасибо!


От переводчиков


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


Данную статью совместными усилиями перевели nlinker, funkill, Hirrolot и blandger.

Подробнее..

Перевод Rust 1.46.0 track_caller и улучшения const fn

28.08.2020 22:20:21 | Автор: admin

Команда Rust рада объявить о новой версии Rust 1.46.0. Rust это язык программирования, который позволяет каждому создавать надёжное и эффективное программное обеспечение.


Если у вас установлена предыдущая версия Rust через rustup, получить Rust 1.46.0 так же просто, как:


rustup update stable

Если у вас ещё не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта и ознакомиться с подробными примечаниями к выпуску 1.46.0 на GitHub.


Что вошло в стабильную версию 1.46.0


Этот выпуск достаточно небольшой, с улучшениями в const fn, стабилизацией двух новых API в стандартной библиотеке и одной опции, удобной для авторов библиотек. Смотрите подробные примечания к выпуску чтобы узнать о других изменениях, не представленных в данном анонсе.


#[track_caller]


Вернёмся в март, когда был выпущен Rust 1.42, в котором улучшились сообщения об ошибках, когда unwrap и подобные функции приводили к панике. Тогда мы упоминали, что реализация ещё не стабильна. В Rust 1.46 мы её стабилизировали.
Этот атрибут зовётся #[track_caller] и был предложен в RFC 2091 аж в июле 2017! Если вы пишете функцию, подобную unwrap, которая может приводить к панике, вы можете добавить эту аннотацию к вашей функции, и модуль форматирования паники по умолчанию будет использовать её для отображения сообщения об ошибке. Например, unwrap:


pub fn unwrap(self) -> T {    match self {        Some(val) => val,        None => panic!("вызван `Option::unwrap()` на значении `None`"),    }}

Теперь будет выглядеть так:


#[track_caller]pub fn unwrap(self) -> T {    match self {        Some(val) => val,        None => panic!("вызван `Option::unwrap()` на значении `None`"),    }}

Вот и всё!


Если вы сами реализуете ловушку паники, вы можете использовать метод вызывающей стороны в std::panic::Location, чтобы получить доступ к этой информации.


Улучшения const fn


Теперь вы также можете использовать в const fn некоторые базовые языковые вещи, такие как:


  • if, if let и match
  • while, while let и loop
  • операторы && и ||

А также преобразование в срез:


const fn foo() {  let x = [1, 2, 3, 4, 5];  // преобразовываем массив в срез  let y: &[_] = &x;}

Хотя эти функции могут не казаться вам новыми, учитывая, что вы можете использовать их все помимо const fn, они добавляют много вычислительной мощности во время компиляции! Как
например, библиотека const-sha1 может позволить вам вычислить хэши SHA1 во время компиляции. Это привело к 40-кратному повышению производительности в Microsoft WinRT для Rust.


Изменения в библиотеке


На волне улучшений в const fn, std::mem::forget теперь тоже const fn. Дополнительно в этом выпуске были стабилизированы два новых API:



Для получения более детальной информации, смотрите подробные примечания к выпуску.


Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.46.0


Множество людей собрались вместе, чтобы создать Rust 1.46.0. Мы не смогли бы сделать это без всех вас. Спасибо!


От переводчиков


С любыми вопросами по языку Rust вам смогут помочь в русскоязычном Телеграм-чате или же в аналогичном чате для новичковых вопросов. Если у вас есть вопросы по переводам или хотите помогать с ними, то обращайтесь в чат переводчиков.
Так же можете поддержать нас на opencollective: https://opencollective.com/rust-lang-ru.


Данную статью совместными усилиями перевели funkill, Hirrolot и andreevlex.

Подробнее..

Перевод Rust 1.47.0 const generics для массивов, LLVM 11, Control Flow Guard и сокращение трассировок

09.10.2020 12:10:51 | Автор: admin

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


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.47.0 вам достаточно выполнить следующую команду:


rustup update stable

Если у вас ещё не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта, а также посмотреть подробные примечания к выпуску на GitHub.


Что вошло в стабильную версию 1.47.0


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


Типажи на больших массивах


Rust всё ещё не поддерживает обобщения для целочисленных значений. Это давно вызывает проблемы с массивами, так как они имеют целочисленную часть в своём типе. [T; N] представляет массив длины N со значениями типа T. Поскольку нет способа сделать его обобщённым по N, типажи для массивов приходится реализовывать вручную для каждого нужного вам N. Для стандартной библиотеки было решено поддерживать N до 32.


Мы работали над особенностью, называемой "const generics" ("константные обобщения"), которая позволяет вам обобщать по N. Полное описание константных обобщений выходит за рамки данного анонса, так как они ещё до конца не стабилизированы. Однако, основная их часть реализована в компиляторе и было принято решение, что константные обобщения достаточно готовы, чтобы использовать их в стандартной библиотеке для массивов любой длины. На практике это означает, что если вы попытаетесь сделать нечто подобное в Rust 1.46:


fn main() {    let xs = [0; 34];    println!("{:?}", xs);}

то получите такую ошибку:


error[E0277]: arrays only have std trait implementations for lengths 0..=32 --> src/main.rs:4:22  |4 |     println!("{:?}", xs);  |                      ^^ the trait `std::array::LengthAtMost32` is not implemented for `[{integer}; 34]`  |  = note: required because of the requirements on the impl of `std::fmt::Debug` for `[{integer}; 34]`  = note: required by `std::fmt::Debug::fmt`  = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

Но в Rust 1.47 массив распечатается правильно.


Это должно сделать массивы более полезными, хотя придётся подождать, пока константные обобщения не стабилизируются и библиотеки смогут использовать их для своих собственных типажей. У нас пока нет предположительной даты стабилизации константных обобщений.


Более короткие трассировки


Вернёмся в Rust 1.18, где мы внесли некоторые изменения в трассировки, которые rustc добавлял при панике. В них есть несколько вещей, которые большую часть времени бесполезны. Однако, в некоторых случаях они регрессировали. В Rust 1.47 виновник нашёлся и был исправлен. С момента регресса эта программа:


fn main() {    panic!();}

давала подобную трассировку:


thread 'main' panicked at 'explicit panic', src/main.rs:2:5stack backtrace:   0: backtrace::backtrace::libunwind::trace             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/libunwind.rs:86   1: backtrace::backtrace::trace_unsynchronized             at /cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/mod.rs:66   2: std::sys_common::backtrace::_print_fmt             at src/libstd/sys_common/backtrace.rs:78   3: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt             at src/libstd/sys_common/backtrace.rs:59   4: core::fmt::write             at src/libcore/fmt/mod.rs:1076   5: std::io::Write::write_fmt             at src/libstd/io/mod.rs:1537   6: std::sys_common::backtrace::_print             at src/libstd/sys_common/backtrace.rs:62   7: std::sys_common::backtrace::print             at src/libstd/sys_common/backtrace.rs:49   8: std::panicking::default_hook::{{closure}}             at src/libstd/panicking.rs:198   9: std::panicking::default_hook             at src/libstd/panicking.rs:217  10: std::panicking::rust_panic_with_hook             at src/libstd/panicking.rs:526  11: std::panicking::begin_panic             at /rustc/04488afe34512aa4c33566eb16d8c912a3ae04f9/src/libstd/panicking.rs:456  12: playground::main             at src/main.rs:2  13: std::rt::lang_start::{{closure}}             at /rustc/04488afe34512aa4c33566eb16d8c912a3ae04f9/src/libstd/rt.rs:67  14: std::rt::lang_start_internal::{{closure}}             at src/libstd/rt.rs:52  15: std::panicking::try::do_call             at src/libstd/panicking.rs:348  16: std::panicking::try             at src/libstd/panicking.rs:325  17: std::panic::catch_unwind             at src/libstd/panic.rs:394  18: std::rt::lang_start_internal             at src/libstd/rt.rs:51  19: std::rt::lang_start             at /rustc/04488afe34512aa4c33566eb16d8c912a3ae04f9/src/libstd/rt.rs:67  20: main  21: __libc_start_main  22: _start

Теперь, с Rust 1.47.0, вместо неё вы увидите следующее:


thread 'main' panicked at 'explicit panic', src/main.rs:2:5stack backtrace:   0: std::panicking::begin_panic             at /rustc/d6646f64790018719caebeafd352a92adfa1d75a/library/std/src/panicking.rs:497   1: playground::main             at ./src/main.rs:2   2: core::ops::function::FnOnce::call_once             at /rustc/d6646f64790018719caebeafd352a92adfa1d75a/library/core/src/ops/function.rs:227

Это позволяет намного легче увидеть, где произошла паника, но вы всё также можете установить RUST_BACKTRACE=full, если хотите видеть всё.


LLVM 11


Мы обновились до LLVM 11. Компилятор по-прежнему может собираться с версиями LLVM до 8, но по умолчанию использует 11 версию.


Control Flow Guard на Windows


rustc теперь поддерживает -C control-flow-guard, опцию, которая включает Control Flow Guard на Windows. Для других платформ этот флаг игнорируется.


Изменения в стандартной библиотеке


Дополнительно в этом выпуске были стабилизированы девять новых API:



Следующие, ранее стабилизированные API, стали const:



Для получения более детальной информации, смотрите подробные примечания к выпуску.


Другие изменения


Rustdoc начал поддерживать тему Ayu.


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.47.0


Множество людей собрались вместе, чтобы создать Rust 1.47.0. Мы не смогли бы сделать это без всех вас. Спасибо!


От переводчиков


С любыми вопросами по языку Rust вам смогут помочь в русскоязычном Телеграм-чате или же в аналогичном чате для новичковых вопросов. Если у вас есть вопросы по переводам или хотите помогать с ними, то обращайтесь в чат переводчиков.
Так же можете поддержать нас на opencollective: https://opencollective.com/rust-lang-ru.


Данную статью совместными усилиями перевели ArtemZdor, fan-tom, funkill, blandger и andreevlex.

Подробнее..

Перевод Rust 1.49.0 aarch64 и улучшения во фреймворке тестирования

02.01.2021 16:06:38 | Автор: admin

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


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.49.0 вам достаточно выполнить следующую команду:


rustup update stable

Если у вас ещё не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта, а также посмотреть подробные примечания к выпуску на GitHub.


Что вошло в стабильную версию 1.49.0


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


64-bit ARM Linux перешёл в Tier 1


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


  • Tier 3 технически, платформы поддерживаются компилятором, но мы не проверяли собирается ли на них код или проходят ли тесты, и мы не предоставляем каких-либо бинарных артефактов, как часть наших релизов.
  • Платформы из Tier 2 гарантированно собираются, но мы не запускаем для них тесты: предоставляемые артефакты могут быть не рабочими или быть с багами.
  • Для платформ из Tier 1 предоставляются наибольшие гарантии и мы запускаем все тесты для этих платформ для каждого изменения, влитого в компилятор. Также мы предоставляем для них собранные артефакты.

Начиная с Rust 1.49.0 платформа aarch64-unknown-linux-gnu передвигается на уровень поддержки Tier 1, предоставляя наши наибольшие гарантии для пользователей Linux, запущенных на 64-bit ARM системах! Мы ожидаем, что данное изменение принесёт пользу для всех: от встраиваемых систем до обычных компьютеров и серверов.


Это важный этап для нашего проекта, поскольку это первая не-x86 платформа, получившая Tier 1 поддержку: мы надеемся, этот шаг откроет путь для большего количества платформ, которые смогу достичь нашего высшего уровня поддержки.


Обратим внимание, что Android не затрагивается данным изменением, так как он представлен другой платформой, находящейся в Tier 2.


64-bit ARM macOS и Windows переходят в Tier 2


В Rust 1.49.0 ещё две платформы достигли Tier 2:


  • Платформа aarch64-apple-darwin предоставляет поддержку Rust на системах Apple M1.
  • aarch64-pc-windows-msvc предоставляет поддержку Rust на 64-bit ARM устройствах, работающих под Windows.

Теперь разработчики могут установить обе эти платформы при помощи rustup! Команда Rust не запускает тесты для данных платформ, так что они могут содержать баги или быть нестабильными.


Фреймворк для тестирования захватывает вывод из потоков


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


#[test]fn thready_pass() {    println!("fee");    std::thread::spawn(|| {        println!("fie");        println!("foe");    })    .join()    .unwrap();    println!("fum");}

Вот как выглядел запуск этого теста до Rust 1.49.0:


 cargo +1.48.0 test   Compiling threadtest v0.1.0 (C:\threadtest)    Finished test [unoptimized + debuginfo] target(s) in 0.38s     Running target\debug\deps\threadtest-02f42ffd9836cae5.exerunning 1 testfiefoetest thready_pass ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out   Doc-tests threadtestrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Вы можете заметить, что вывод из потоков напечатался и смешался с выводом самого фреймворка. Было бы неплохо, если бы каждый println! работал также, как тот, что напечатал "fum"? Что ж, таково поведение в Rust 1.49.0:


 cargo test   Compiling threadtest v0.1.0 (C:\threadtest)    Finished test [unoptimized + debuginfo] target(s) in 0.52s     Running target\debug\deps\threadtest-40aabfaa345584be.exerunning 1 testtest thready_pass ... oktest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s   Doc-tests threadtestrunning 0 teststest result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Но не расстраивайтесь, если тест упадёт, вы увидите всё, что он хотел вывести. Добавив в конец теста panic!, вы можете увидеть примерно такую ошибку:


 cargo test   Compiling threadtest v0.1.0 (C:\threadtest)    Finished test [unoptimized + debuginfo] target(s) in 0.52s     Running target\debug\deps\threadtest-40aabfaa345584be.exerunning 1 testtest thready_pass ... FAILEDfailures:---- thready_pass stdout ----feefiefoefumthread 'thready_pass' panicked at 'explicit panic', src\lib.rs:11:5

В частности, средство запуска тестов обеспечивает захват вывода и сохраняет его в случае сбоя теста.


Изменения в стандартной библиотеке


В Rust 1.49.0 были стабилизированы следующие 3 функции:



И ещё две функции стали const:



Смотрите подробные примечания к выпуску для более детальной информации.


Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.49.0


Множество людей собрались вместе, чтобы создать Rust 1.49.0. Мы не смогли бы сделать это без всех вас. Спасибо!


От переводчиков


С любыми вопросами по языку Rust вам смогут помочь в русскоязычном Телеграм-чате или же в аналогичном чате для новичковых вопросов. Если у вас есть вопросы по переводам или хотите помогать с ними, то обращайтесь в чат переводчиков.
Так же можете поддержать нас на OpenCollective.


Данную статью совместными усилиями перевели andreevlex, funkill, ozkriff, blandger и fan-tom.

Подробнее..

Перевод Rust 1.50.0 улучшение индексации массивов, безопасность полей объединений и усовершенствование файловых дескрипторов

12.02.2021 12:08:58 | Автор: admin

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


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.50.0 вам достаточно выполнить следующую команду:


rustup update stable

Если у вас ещё не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта, а также посмотреть подробные примечания к выпуску на GitHub.


Что вошло в стабильную версию 1.50.0


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


Константные обобщения при индексации массива


Продолжая движение к стабилизации константных обобщений, этот выпуск добавляет реализации ops::Index и IndexMut для массивов [T; N] любой длины const N. Оператор индексации [] уже работал с массивами с помощью встроенной магии компилятора, но на уровне типа массивы до сих пор фактически не реализовывали библиотечные типажи.


fn second<C>(container: &C) -> &C::Outputwhere    C: std::ops::Index<usize> + ?Sized,{    &container[1]}fn main() {    let array: [i32; 3] = [1, 2, 3];    assert_eq!(second(&array[..]), &2); // срезы работали ранее    assert_eq!(second(&array), &2); // теперь это работает напрямую}

const повторение значений массива


Массивы в Rust могут быть записаны как в форме списков [a, b, c], так и в форме повторений [x; N]. Повторения разрешены для длины N большей, чем один, только для x, реализующих типаж Copy, и в рамках RFC 2203 мы стремились разрешить любые const выражения. Однако пока эта функциональность была нестабильна для произвольных выражений, лишь начиная с Rust 1.38 её реализация случайно позволила использовать const значения в повторениях массивов.


fn main() {    // Это не разрешено, так как `Option<Vec<i32>>` не реализует `Copy`.    let array: [Option<Vec<i32>>; 10] = [None; 10];    const NONE: Option<Vec<i32>> = None;    const EMPTY: Option<Vec<i32>> = Some(Vec::new());    // Однако повторения с `const` значениями разрешены!    let nones = [NONE; 10];    let empties = [EMPTY; 10];}

В Rust 1.50 эта возможность признана официально, так что вы можете использовать такие конструкции без опасений. В будущем для того, чтобы избежать "временных" именований констант, вы можете использовать встроенные выражения const согласно RFC 2920.


Безопасные присвоения полям объединения ManuallyDrop<T>


В Rust 1.49 появилась возможность добавлять поля ManuallyDrop<T> в union, позволяющая таким образом использовать Drop для объединений. Однако объединения не сбрасывают старые значения во время присваивания полей, так как не знают, какой из вариантов ранее был действителен. Поэтому из соображений безопасности Rust ранее ограничивался только типами Copy, которые не используют Drop. Разумеется, ManuallyDrop<T> не требует Drop, поэтому теперь Rust 1.50 расценивает присваивания и этим полям как безопасные.


Ниша для File на Unix платформах


У некоторых типов в Rust есть определённые ограничения на то, какое значение считать допустимым, поскольку они могут выходить за границы допустимых значений диапазона памяти. Мы называем любое оставшееся недопустимое значение нишей (niche), и это пространство может быть использовано для оптимизации схемы размещения типов. Например, в Rust 1.28 мы представили целочисленные типы NonZero, такие как NonZeroU8, где 0 ниша. Это позволило Option<NonZero> использовать 0, чтобы представить None без использования дополнительной памяти.


На Unix-платформах File это просто системный целочисленный файловый дескриптор. Это значит, что у нас есть возможная ниша ведь он никогда не может быть -1! Системные вызовы, которые возвращают файловый дескриптор, используют -1 для обозначения того, что произошла ошибка (проверяется errno), так что -1 никогда не будет действительным файловым дескриптором. Начиная с Rust 1.50 эта ниша добавлена в определение типа и тоже может быть использована для оптимизации размещения значения в памяти. И следовательно Option<File> теперь имеет такой же размер, как и File!


Изменения в стандартной библиотеке


В Rust 1.50.0 были стабилизированы следующие 9 функций:



И довольно много существующих функций стало const:


  • IpAddr::is_ipv4
  • IpAddr::is_ipv6
  • Layout::size
  • Layout::align
  • Layout::from_size_align
  • pow для всех целочисленных типов
  • checked_pow для всех целочисленных типов
  • saturating_pow для всех целочисленных типов
  • wrapping_pow для всех целочисленных типов
  • next_power_of_two для всех беззнаковых целочисленных типов
  • checked_power_of_two для всех беззнаковых целочисленных типов

Смотрите подробные примечания к выпуску для более детальной информации.


Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.50.0


Множество людей собрались вместе, чтобы создать Rust 1.50.0. Мы не смогли бы сделать это без всех вас. Спасибо!


От переводчиков


С любыми вопросами по языку Rust вам смогут помочь в русскоязычном Телеграм-чате или же в аналогичном чате для новичковых вопросов. Если у вас есть вопросы по переводам или хотите помогать с ними, то обращайтесь в чат переводчиков.
Также можете поддержать нас на OpenCollective.


Данную статью совместными усилиями перевели andreevlex, TelegaOvoshey, blandger, nlinker и funkill.

Подробнее..

Перевод Планирование редакции Rust 2021

05.03.2021 22:04:25 | Автор: admin

Рабочая группа Rust 2021 Edition рада сообщить, что следующая редакция Rust Rust 2021 запланирована на этот год. Пока что формально описывающий её RFC остаётся открытым, но мы ожидаем, что в скором времени он будет принят. Планирование и подготовка уже начались, и мы идём по графику!


Если вам интересно, какие новшества появятся в Rust 2021 или когда эта редакция выйдет в стабильной версии, читайте нашу статью!


Что входит в эту редакцию?


Конечный список нововведений, которые войдут в Rust 2021, ещё не определён до конца. В целом мы планируем, что выпуск Rust 2021 будет намного меньше, чем Rust 2018, по следующим причинам:


  • Ритм выпусков стал регулярным Это значит, что мы будем активно использовать плюсы "цепочечной" модели на уровне редакций Rust.
  • Редакция Rust 2018 выбилась из модели "минимального стресса" выпусков.
  • Сейчас просто нужно меньше фундаментальных изменений, чтобы язык продолжал развиваться.

Более подробно о развитии концепции редакций вы можете почитать в RFC.


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


Изменения в прелюдии


В то время как типы и свободные функции могут быть добавлены в прелюдию без привязки к редакции, с трейтами всё иначе. Добавление трейта в прелюдию может вызвать проблемы совместимости например, вызов метода с именем, совпадающим с именем метода нового трейта в области видимости, может привести к двусмысленности.


Сейчас в редакцию Rust 2021 предложено включить следующие трейты:


  • TryFrom/TryInto
  • FromIterator

RFC с этими изменениями можно найти тут. Обратите внимание, что RFC ещё не принят состав новой прелюдии активно обсуждается.


Новые правила захвата


RFC 2229 предлагает, когда это возможно, захватывать замыканиями отдельные поля, а не всю структуру целиком. Этот RFC уже принят. В некоторых случаях такое изменение может заставить деструкторы запускаться в другом месте, нежели сейчас, из-за чего оно должно быть привязано к редакции. Миграция анализаторов должна свести к минимум изменения в семантике уже существующего кода.


Новый распознаватель функциональности в Cargo по умолчанию


В Rust 1.51 будет стабилизирован новый распознаватель функциональности в Cargo, который разрешит зависимостям пакета использовать разную функциональность в разных контекстах. Например, пакет с #[no_std] сможет использовать одну и ту же зависимость и во время сборки (build-dependencies с включённым std), и как обычную зависимость (без std). Пока что это приводит к тому, что std будет включена в обоих случаях, так как функциональность находится в глобальном пространстве имён.


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


Прочие изменения


Другие предложенные изменения включают унификацию работы panic в std и core и обновление уровня некоторых проверок с предупреждений до ошибок.


Полный список обсуждаемых функций вы можете найти здесь.


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


Примерный график


Итак, когда же мы планируем выпустить новую редакцию? Вот график основных этапов, к которому мы стремимся:


  • 1 апреля все релевантные редакции RFC или приняты, или в хорошем состоянии (т. е. все основные вопросы решены, и принятие RFC произойдёт в ближайшие недели).
  • 1 мая все нововведения, включённые в Rust 2021, находятся в Nightly с соответствующими feature-флагами.
  • 1 июня все проверки добавлены в Nightly.
  • 1 сентября редакция стабилизирована в Nightly.
  • 21 октября редакция полностью стабилизирована.

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


Приглашаем к участию


Если вы хотите помочь в выпуске редакции Rust 2021 пожалуйста, свяжитесь с нами. Помимо работы над функциональностью и планированием выпуска, у будет ещё очень много других важных дел. Вот только часть того, что необходимо для успешного выпуска:


  • rustfix миграции для всех соответствующих функций,
  • тестирование всех функций и путей их миграции,
  • сообщения в блогах и другие маркетинговые материалы.

От переводчиков


С любыми вопросами по языку Rust вам смогут помочь в русскоязычном Телеграм-чате или же в аналогичном чате для новичковых вопросов. Если у вас есть вопросы по переводам или хотите помогать с ними, то обращайтесь в чат переводчиков.
Также можете поддержать нас на OpenCollective.


Данную статью совместными усилиями перевели blandger, TelegaOvoshey, funkill и andreevlex.

Подробнее..

Перевод Неблокирующие паттерны атомарные операции и частичные барьеры памяти

13.03.2021 16:07:18 | Автор: admin

В первой статье цикла мы познакомились с простыми неблокирующими алгоритмами, а также рассмотрели отношение happens before, позволяющее их формализовать. Следующим шагом мы рассмотрим понятие гонки данных (data race), а также примитивы, которые позволяют избежать гонок данных. После этого познакомимся с атомарными примитивами, барьерами памяти, а также их использованием в механизме seqcount.


С барьерами памяти некоторые разработчики ядра Linux уже давно знакомы. Первый документ, содержащий что-то похожее на спецификацию гарантий, предоставляемых ядром при одновременном доступе к памяти он так и называется: memory-barriers.txt. В этом файле описывается целый зоопарк барьеров вместе с ожидаемым поведением многопоточного кода вядре. Также там описывается понятие парных барьеров (barrier pairing), что похоже на пары release-acquire операций и тоже помогает упорядочивать работу потоков.


В этой статье мы не будем закапываться так же глубоко, как memory-barriers.txt. Вместо этого мы сравним барьеры с моделью acquire и release-операций и рассмотрим, как они упрощают (или, можно сказать, делают возможной) реализацию примитива seqcount. К сожалению, даже если ограничиться лишь наиболее популярными применениями барьеров это слишком обширная тема, поэтому о полных барьерах памяти мы поговорим в следующий раз.



Гонки данных и атомарные операции


Рассматриваемое здесь определение гонок данных впервые сформулировано в C++11 и с тех пор используется многими другими языками, в частности, C11 и Rust. Все эти языки довольно строго относятся к совместному доступу к данным без использования мьютексов: так позволяется делать только со специальными атомарными типами данных, используя атомарное чтение и атомарную запись в память.


Гонка данных возникает между двумя операциями доступа к памяти, если 1) они происходят одновременно (то есть, не упорядочены отношением A происходит перед B), 2) одна из этих операций это запись, и 3) хотя бы одна из операций не является атомарной. Врезультате гонки данных (сточки зрения C11/C++11) может произойти что угодно неопределённое поведение. Отсутствие гонок данных ещё не означает невозможность состояний гонки (race conditions) валгортимах: гонка данных это нарушение стандарта языка, а состояние гонки это ошибка вреализации алгортима, вызванная неправильным использованием мьютексов, acquire-release семантики, или и того и другого.


Избежать гонок данных и вызываемого ими неопределённого поведения очень легко. Самый простой способ это работать с данными только из одного потока. Если данные должны быть доступны другим потокам, то работа с ними должна быть упорядочена нужным вам образом с помощью acquire и release-операций. Наконец, вы можете воспользоваться атомарными операциями.


C11, C++11, Rust предоставляют целый спектр атомарных операций доступа к памяти, гарантирующих некоторый порядок доступа (memory ordering). Нас интересуют три вида: acquire (для чтения), release (для записи), и relaxed (для того и другого). Что делают acquire и release вам уже должно быть ясно, в ядре Linux это называется smp_load_acquire() и smp_store_release(). А вот relaxed-операции обеспечивают так называемый нестрогий порядок доступа к памяти. Нестрогие операции не создают никаких отношений порядка между потоками. Их единственная задача это предотвратить гонку данных и избежать нарушения строгой буквы стандарта.


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


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


Эти макросы уже встречались в первой части:


    поток 1                           поток 2    -----------------------------     ------------------------    a.x = 1;    smp_wmb();    WRITE_ONCE(message, &a);          datum = READ_ONCE(message);                                      smp_rmb();                                      if (datum != NULL)                                        printk("%x\n", datum->x);

Они похожи на smp_load_acquire() и smp_store_release(), но первый аргумент тут является lvalue, а неуказателем (внимание на присваивание message в потоке1). При отсутствии иных механизмов, избегающих гонок данных (вроде захваченного спинлока), настоятельно рекомендуется использовать READ_ONCE() и WRITE_ONCE() при обращении к данным, доступным другим потокам. Сами эти операции не упорядочены, но всегда используются вместе с чем-то ещё, вроде другого примитива или механизма синхронизации, который уже обладает release и acquire семантикой. Так атомарные операции оказываются в итоге упорядочены нужным образом.


Пусть, например, у вас есть struct work_struct, которые в фоне затирают ненужные массивы единичками. После запуска задачи у вас есть другие важные дела и массив вам не нужен. Когда понадобится, то вы делаете flush_work() и гарантированно получаете единички. flush_work(), как и pthread_join(), обладает acquire-семантикой и синхронизируется с завершением struct work_struct. Поэтому читать из массива можно и обычными операциями чтения, которые гарантированно произойдут после записи, выполненной задачей. Однако, если вы забиваете единичками регионы, которые могут пересекаться и обновляться из нескольких потоков, то им следует использовать WRITE_ONCE(a[x],1), а не просто a[x]=1.


Всё становится сложнее, если release и acquire-семантика обеспечивается барьерами памяти. Рассмотрим в качестве примера реальный механизм seqcount.



Seqcounts


Seqcount (sequence counter) это специализированный примитив, сообщающий вам, а не изменилась ли структура данных, пока вы с ней работали. У seqcounts довольно узкая зона применимости, где они показывают себя хорошо: защищаемых ими данных должно быть немного, при чтении не должно быть никаких побочных эффектов, а записи должны быть сравнительно быстрыми и редкими. Но зато читатели никогда не блокируют писателей и никак не влияют на их кеш. Эти довольно существенные преимущества, когда вам нужна масштабируемость.


Seqcount работает с одним писателем и множеством читателей. Обычно seqcount комбинируется смьютексом или спинлоком для того, чтобы гарантировать эксклюзивный доступ писателям; врезультате получается примитив seqlock_t, как его называют в Linux. Вне ядра слова seqlock и seqcount порой используются как синонимы.


Фактически, seqcount это счётчик поколений. Нечётный номер поколения означает, что в этот момент времени со структурой данных работает писатель. Если читатель увидел нечётный номер на входе в критическую секцию или если номер изменился на выходе из критической секции, то структура данных возможно изменялась. Читатель мог увидеть лишь несогласованную часть этих изменений, так что ему следует повторить всю свою работу с начала. Для корректного функционирования seqcount читатель должен корректно опознавать начало и конец работы писателя. По паре load-acquire и store-release операций на каждую сторону. Если раскрыть все макросы, то простая [и неправильная] реализация seqcount на уровне отдельных операций с памятью выглядит примерно так:


    поток 1 (писатель)                 поток 2 (читатель)    --------------------------------    ------------------------                                        do {    WRITE_ONCE(sc, sc + 1);                 old = smp_load_acquire(&sc) & ~1;    smp_store_release(&data.x, 123);        copy.y = READ_ONCE(data.y);    WRITE_ONCE(data.y, 456);                copy.x = smp_load_acquire(&data.x);    smp_store_release(&sc, sc + 1);     } while(READ_ONCE(sc) != old);

Этот код слегка похож на передачу сообщений из первой части. Здесь видно две пары load-acquire store-release операций: для sc и для data.x. И довольно легко показать, что они обе необходимы:


  • Когда поток 2 выполняется после потока1, то при первом чтении sc он должен увидеть значение, которое поток1 туда записал вторым присваиванием. Пара smp_store_release() и smp_load_acquire() здесь гарантирует, что чтение произойдёт после записи.
  • Когда потоки исполняются одновременно, то если поток2 уже увидел новое значение какого-либо поля пусть data.x то он должен увидеть и новое значение sc при проверке цикла. Пара smp_store_release() и smp_load_acquire() здесь гарантирует, что как минимум первый инкремент sc будет видно и поток 2 уйдёт на второй круг.

Вопрос на самопроверку: зачем читатель делает & ~1?


Но если внимательно присмотреться и подумать*, то в коде есть хитрая ошибка! Так как писатель неделает ни одной acquire-операции, то присваивание data.y в принципе может произойти ещё до первого инкремента sc. Конечно, можно психануть и делать вообще всё исключительно через load-acquire/store-release, но это пальба из пушки по воробьям и только маскирует проблему. Если подумать ещё чуть-чуть, то можно найти правильное и эффективное решение.
________
* Вот я, например, сразу не заметил этой ошибки и мне уже в комментариях подсказали.


В первой статье мы видели, что порой в Linux используют WRITE_ONCE() и smp_wmb() вместо smp_store_release(). Аналогично, smp_rmb() и READ_ONCE() вместо smp_load_acquire(). Эти частичные барьеры памяти создают особый тип отношений порядка между потоками. А именно, smp_wmb() делает все последующие неупорядоченные присваивания release-операциями, а smp_rmb(), соответственно, превращает предыдущие неупорядоченные чтения в load-acquire. (Строго говоря, это несовсем так, но примерно так о них можно думать.)


Попробуем улучшить работу с полями data:


    поток 1 (писатель)                 поток 2 (читатель)    ------------------------------      ------------------------    // write_seqcount_begin(&sc)        do {    WRITE_ONCE(sc, sc + 1);                 // read_seqcount_begin(&sc)    smp_wmb();                              old = smp_load_acquire(&sc) & ~1;    WRITE_ONCE(data.x, 123);                copy.y = READ_ONCE(data.y);    WRITE_ONCE(data.y, 456);                copy.x = READ_ONCE(data.x);    // write_seqcount_end(&sc)              // read_seqcount_retry(&sc, old)    smp_store_release(&sc, sc + 1);         smp_rmb();                                        } while(READ_ONCE(sc) != old);

Даже если не знать семантики smp_wmb() и smp_rmb(), любому программисту очевидно, что такой код гораздо проще завернуть в удобный API. С данными можно работать, используя обычные атомарные операции (а модель памяти Linux даже позволяет и неатомарные), тогда как волшебные барьеры можно спрятать за read_seqcount_retry() и write_seqcount_begin().


Добавленные барьеры разделяют READ_ONCE() и WRITE_ONCE() на группы, обеспечивая безопасность работы seqcount. Но тут есть пара нюансов:


  • Во-первых, неупорядоченные атомарные операции остаются неупорядоченными. Поток 2 может увидеть новое значение data.y вместе со старым data.x. Для seqcount это непроблема, так как последующая проверка sc приведёт к повтору цикла.
  • Во-вторых, барьеры дают меньше гарантий, чем load-acquire и store-release. Чтение через smp_load_acquire() гарантированно происходит перед чтениями и записями в память, которые следуют за ним. Аналогично, присваивание через smp_store_release() происходит не только после предыдущих записей в память, но и чтений в том числе. Тогда как smp_rmb() упорядочивает лишь чтения, а smp_wmb() только записи. Правда, взаимный порядок между чтениями и записями, наблюдаемый из других потоков редко важен на практике именно по этой причине в ядре долгое время использовались только smp_rmb() и smp_wmb().

В случае seqcount порядок между чтениями и записями не особо важен, так как читатель никогда не пишет в общую память. В потоке писателя же операции очевидно упорядочены как надо. Логика тут слегка неочевидная размышляйте, пока не дойдёт. Пока код остаётся таким же простым, как и в примере, то он будет работать. Если читателю всё же потребуется изменять память, то для этих присваиваний потребуется уже что-то другое, не seqcount.


Предыдущий абзац очень неформально всё описывает, но это хорошая иллюстрация того, почему важно знать паттерны неблокирующего программирования. Это знание позволяет размышлять окоде на более высоком уровне без потери точности. Вместо того, чтобы описывать каждую инструкцию отдельно, вы можете просто сказать: data.x и data.y защищены seqcount sc. Иликаквпредыдущем примере: a передаётся другому потоку через message. Мастерство неблокирующего программирования отчасти состоит в умении узнавать и использовать подобные паттерны, облегчающие понимание кода.


На этом, пожалуй, пока и остановимся. Естественно, тема барьеров далеко не исчерпана, поэтому вследующей статье мы рассмотрим полные барьеры памяти, как барьеры вообще работают, и ещё больше примеров их использования в ядре Linux.

Подробнее..

Перевод Приёмы неблокирующего программирования полные барьеры памяти

26.03.2021 04:11:58 | Автор: admin

В первых двух статьях цикла мы рассмотрели четыре способа упорядочить доступ к памяти: load-acquire и store-release операции впервой части, барьеры чтения и записи в память вовторой. Теперь пришла очередь познакомиться с полными барьерами памяти, их влиянием на производительность, и примерами использования полных барьеров в ядре Linux.


Рассмотренные ранее примитивы ограничивают возможный порядок исполнения операций спамятью четырьмя различными способами:


  • Load-acquire операции выполняются перед последующими чтениями и записями.
  • Store-release операции выполняются после предыдущих чтений и записей.
  • Барьеры чтения разделяют предыдущие и последующие чтения из памяти.
  • Барьеры записи разделяют предыдущие и последующие записи в память.

Внимательный читатель заметил, что одна из возможных комбинаций в этом списке отсутствует:

Чтение выполняется... Запись выполняется...
после чтения smp_load_acquire(), smp_rmb() smp_load_acquire(), smp_store_release()
после записи ??? smp_store_release(), smp_wmb()
Оказывается, обеспечить глобальный порядок записей и последующих чтений из памяти гораздо сложнее. Процессоры вынуждены прилагать отдельные усилия для этого. Сохранение такого порядка стоит недёшево и требует явных инструкций. Чтобы понять причину этих особенностей, нам придётся спуститься на уровнь ниже и присмотреться к тому, как процессоры работают спамятью.



Что творится внутри процессоров


В первой статье уже упоминалось, что на самом деле процессоры обмениваются сообщениями через шину вроде QPI или HyperTransport, поддерживая таким образом когерентность кешей. На уровне ассемблера, правда, этого не видно. Там есть только инструкции записи и чтения из памяти. Acquire и release-семантика отдельных операций реализуется уже конвеером исполнения инструкций конкретного процессора, с учём его архитектурных особенностей.


Например, на x86-процессорах любое чтение из памяти это load-acquire операция, а любая запись это store-release, потому что так того требует спецификация архитектуры. Тем не менее, это ещё незначит, что в коде для x86 можно никак не обозначать acquire и release-операции. Барьеры влияют не только на процессор, но и на оптимизации компилятора, которые тоже могут переупорядочивать операции с памятью.


Кроме того, архитектура x86 негарантирует, что все процессоры будут видеть операции с памятью в одинаковом порядке. Рассмотрим следующий пример, где по адресам a и b изначально расположены нули:


    CPU 1                    CPU 2    -------------------      --------------------    store 1 => a             store 1 => b    load  b => x             load  a => y

Как бы вы ни переставляли эти операции между собой, как минимум одна из записей в память будет находиться перед соответствующим чтением. Соответственно, можно было бы ожидать, что либо в x, либо в y, либо в обоих окажется единица. Но даже на x86 может случиться так, что ивx, ивy будут считаны нули.


Почему? Дело в том, у каждого процессора есть так называемый буфер записей (store buffer), находящийся между процессором и его L1-кешем. Запись в память обычно изменяет только часть кеш-линии. Если кеш-линия инвалидирована, то процессору сперва надо достать новые данные из памяти аэто медленно. Поэтому новые данные для записи складываются в буфер, который позволяет процессору продолжить работу не ожидая обновления кеша.


Буфер записей не особо мешает процессорам сохранить глобальный порядок операций с памятью, когда это чтениечтение, чтениезапись, записьзапись:


  • Если процессор переупорядочивает инструкции в конвеере для увеличения производительности (out-of-order execution), то механизм спекулятивного исполнения инструкций может сохранять порядок операций относительно чтений из памяти. Процессор начинает отслеживать кеш-линии смомента их чтения и до момента, когда инструкция чтения покидает конвеер. Если на этом промежутке кеш-линия оказывается вытеснена из кеша, то все спекулятивно исполненные и незавершённые операции, зависящие от считанного значения, требуется повторить, считав новое значение из памяти. Если же отслеживаемая кеш-линия остаётся на месте, то все последующие операции спамятью будут завершены после чтения, так как инструкции покидают конвеер и завершают исполение уже впорядке их следования впрограмме.
  • Сохранить порядок записей между собой ещё проще: каждому процессору достаточно переносить записи в кеш в порядке их поступления в буфер записей. Дальше протокол когерентности всё сделает сам.

Но вот гарантировать, что только что записанное одним процессором значение будет считано другим процессором это гораздо сложнее. Во-первых, новое значение может застрять на некоторое время в буфере записей одного процессора, и пока оно не попадёт в L1-кеш, другие процессоры его не увидят. Во-вторых, чтобы процессор всегда видел свои же записи, все чтения сперва проходят через буфер записей (механизм store forwarding). То есть, если у CPU1 или CPU2 вих буферах записей окажутся значения для b и a соответственно, то они увидят именно их предыдущие значения (нули), независимо от состояния кешей.


Единственный способ получить ожидаемое поведение это сбросить весь буфер записей вкеш после записи и перед чтением. Несамая дешёвая операция (пара-тройка десятков циклов), но именно это делает полный барьер памяти smp_mb() в Linux. Рассмотрим теперь, как это выглядит наC:


    поток 1                       поток 2    -------------------           --------------------    WRITE_ONCE(a, 1);             WRITE_ONCE(b, 1);    smp_mb();                     smp_mb();    x = READ_ONCE(b);             y = READ_ONCE(a);

Допустим, в x получается ноль. Что должно для этого произойти? Волнистой линией обозначим ситуацию, когда WRITE_ONCE(b,1) не успевает перезаписать значение, считываемое другим потоком. (Вмодели памяти ядра такое отношение называется from-reads.) Поведение потоков можно описать так:


    WRITE_ONCE(a, 1);           |      -----+----- smp_mb();           |           v    x = READ_ONCE(b);   >  WRITE_ONCE(b, 1);                                         |                                    -----+----- smp_mb();                                         |                                         v                                  y = READ_ONCE(a);

Вспомните, что атомарные операции сами по себе ничего не упорядочивают между потоками. Барьеры тоже не обладают acquire- или release-семантикой. Никаких общих гарантий порядка выполнения операций здесь нет, но есть некоторые специальные гарантии наблюдаемого поведения, на которые всё же можно рассчитывать.


Полный барьер в потоке2 гарантирует, что к моменту выполнения READ_ONCE(a) буфер записей будет сброшен в кеш. Если это произойдёт перед READ_ONCE(b), то она уже увидит запись WRITE_ONCE(b,1) и в x должна будет оказаться единица. Соответственно, если там оказался ноль, порядок выполнения должен быть другим READ_ONCE(b) должна выполниться первой:


    WRITE_ONCE(a, 1);              WRITE_ONCE(b, 1);           |                              |           |                         -----+----- smp_mb();           |                              |           v                              v    x = READ_ONCE(b); -----------> y = READ_ONCE(a);                      (если x = 0)

Благодаря транзитивности, READ_ONCE(a) втаком случае увидит эффект WRITE_ONCE(a,1) и, соответственно, y=1 когда x=0. Аналогично, если второй поток всё ещё видит ноль вa, то полный барьер в первом потоке гарантирует, что READ_ONCE(a) выполнится перед READ_ONCE(b):


    WRITE_ONCE(a, 1);              WRITE_ONCE(b, 1);           |                              |      -----+----- smp_mb();               |           |                              |           v                              v    x = READ_ONCE(b); <----------- y = READ_ONCE(a);                      (если y = 0)

То есть, если y=0, то обязательно x=1. Порядок выполнения операций негарантируется, но каким бы он ни оказался, x и y теперь немогут одновременно содержать нули. Иначе READ_ONCE(a) должна была бы выполниться перед READ_ONCE(b), а READ_ONCE(b) перед READ_ONCE(a), что невозможно.


Модель памяти Linux не считает такие ситуации отношением happens-before между потоками, ведь ни одна из операций не имеет acquire или release-семантики и порядок между ними, строго говоря, не определён. Но тем не менее, барьеры памяти всё же способны влиять на поведение потоков, что позволяет писать высокоуровневые примитивы синхронизации, пользователи которых могут рассчитывать на вполне определённое неопределённое поведение. Рассмотрим теперь, как барьеры применяются на практике.




Синхронизация сна и пробуждения


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


Рассмотрим типичный пример. Пусть один поток хочет попросить другой что-то сделать. Адругой поток часто бывает занят, либо вообще устаёт работать и хочет поспать самоудаляется из планировщика потоков и отправляется на неопределённое время всон. Первому потоку надо растормошить второй. Вкоде это выглядит как-то так:


    поток 1                               поток 2    -------------------                   --------------------------    WRITE_ONCE(dont_sleep, 1);            WRITE_ONCE(wake_me, 1);    smp_mb();                             smp_mb();    if (READ_ONCE(wake_me))               if (!READ_ONCE(dont_sleep))      wake(thread2);                        sleep();

Если второй поток видит в dont_sleep ноль, то первый поток увидит в wake_me единицу иразбудит второй поток. Выглядит, как будто у первого потока release-семантика (представьте, что wake() это как mutex_unlock()). Если же первый поток увидит в wake_me ноль, то второй поток обязательно увидит единицу в dont_sleep и просто непойдёт спать. Второй поток это как бы acquire-половинка операции.


Всё это держится на предположении, что команды первого потока нетеряются. Если, например, wake() вызывается после READ_ONCE() во втором потоке, но до sleep(), то второй поток недолжен витоге заснуть. Как вариант, эти операции могут блокироваться на общем мьютексе. Вот вам ещё один пример того, как неблокирующие приёмы программирования применяются вместе с традиционной синхронизацией. Вданном случае это неявляется проблемой, так как блокировки будут очень редкими.


Приём действительно работает и применяется, например, в интерфейсах prepare_to_wait() и wake_up_process(). Они были добавлены в ядро ещё в ветке 2.5.x, что в своё время подробно разбиралось наLWN. Если раскрыть вызовы функций, то можно увидеть знакомые строки:


    поток 1                                       поток 2    -------------------                           --------------------------    WRITE_ONCE(condition, 1);                     prepare_to_wait(..., TASK_INTERRUPTIBLE) {    wake_up_process(p) {                            set_current_state(TASK_INTERRUPTIBLE) {      try_to_wake_up(p, TASK_NORMAL, 0) {             WRITE_ONCE(current->state, TASK_INTERRUPTIBLE);        smp_mb();                                     smp_mb();        if (READ_ONCE(p->state) & TASK_NORMAL)      }          ttwu_queue(p);                          }      }                                           if (!READ_ONCE(condition))    }                                               schedule();

Как и с seqcount, все барьеры спрятаны за удобным высокоуровневым API. Собственно, как раз использование барьеров или load-acquire/store-release операций и придаёт acquire- или release-семантику всему интерфейсу. Вданном случае wake_up_process() обладает release-семантикой, аset_current_state() распространяет свою acquire-семантику на вызов prepare_to_wait().


Ещё часто бывает, что флажок проверяют дважды, дабы по возможности избежать лишних вызовов wake():


    поток 1                               поток 2    -------------------                   --------------------------    WRITE_ONCE(dont_sleep, 1);            if (!READ_ONCE(dont_sleep)) {    smp_mb();                               WRITE_ONCE(wake_me, 1);    if (READ_ONCE(wake_me))                 smp_mb();      wake(thread2);                        if (!READ_ONCE(dont_sleep))                                              sleep();                                          }

В ядре подобные проверки можно найти в tcp_data_snd_check(), вызываемой из tcp_check_space() одним потоком и tcp_poll() в другом потоке. Код здесь довольно низкоуровневый, так что разберём его подробнее. Если в буфере сокета закончилось место, то надо подождать, пока оно освободится. tcp_poll() в одном потоке устанавливает флаг SOCK_NOSPACE раз места нет, то надо спать перед проверкой __sk_stream_is_writeable(), вот здесь:


    set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);    smp_mb__after_atomic();    if (__sk_stream_is_writeable(sk, 1))      mask |= EPOLLOUT | EPOLLWRNORM;

Если возвращаемая маска будет пустой, то для вызывающего кода это означает, что сокет не готов (и надо ждать, пока будет готов). smp_mb__after_atomic() это специализированная версия smp_mb() с аналогичной семантикой. Коптимизациям барьеров мы ещё вернёмся, но позже.


Другой поток, занятый отправкой данных из сокета, должен впоследствии разбудить первый поток. tcp_data_snd_check() сперва отправляет TCP-пакеты, освобождая место в буфере можно неспать, место появилось затем проверяет флажок SOCK_NOSPACE, и наконец (через указатель нафункцию sk->sk_write_space()) попадает в sk_stream_write_space(), где флажок сбрасывается и если там кто-то спит, то его будят. Вызовов функций тут немного, так что я думаю, вы сами легко разберётесь. Также обратите внимание на комментарий в tcp_check_space():


    /* pairs with tcp_poll() */    smp_mb();    if (test_bit(SOCK_NOSPACE, &sk->sk_socket->flags))      tcp_new_space(sk);

Парное использование барьеров означает, что функции составляют взаимосвязанную пару операций с acquire и release-семантикой. По барьерам чтения или записи в память легко понять, какая у чего семантика: acquire для чтения, release для записи. Сполными барьерами же для понимания семантики следует читать код вокруг них и думать. Внашем случае мы знаем, что функция, инициирующая пробуждение tcp_check_space() обладает release-семантикой. Соответственно, у tcp_poll() acquire-семантика.


Подобный приём идиому можно заметить почти везде, где в ядре используется smp_mb(). Например:


  • Workqueues таким образом решают, может ли рабочий поток отправиться поспать, если ему больше нечего делать. Будильником здесь выступает insert_work(), тогда как wq_worker_sleeping(), очевидно, хочет спать.
  • Системный вызов futex() с одной стороны имеет пользовательский поток, записывающий новое значение в память, а барьеры являются частью futex(FUTEX_WAKE). Сдругой стороны находится поток, выполняющий futex(FUTEX_WAIT) и все операции с флажком wake_me внутри ядра. futex(FUTEX_WAIT) получает через аргумент ожидаемое значение в памяти, и потом решает, надо ли спать или уже нет. См.длинный комментарий в начале kernel/futex.c, где подробно рассматривается этот механизм.
  • В контексте KVM роль сна играет переход процессора в гостевой режим, когда он отдаётся враспоряжение виртуальной машины. Для того, чтобы выбить процессор из рук гостевой ОС ивернуть его себе обратно, kvm_vcpu_kick() отправляет межпроцессорное прерывание. Вглубине стека вызовов можно найти kvm_vcpu_exiting_guest_mode(), где видно знакомые нам комментарии о парных барьерах вокруг флажка vcpu->mode.
  • В драйверах virtio можно найти два места, где smp_mb() используется похожим образом. Содной стороны находится драйвер, который иногда хочет прервать операцию и пинает занятое устройство прерыванием. Сдругой стороны есть устройство, которому иногда надо отсигналить ожидающему драйверу о завершении операции.

На этом знакомство с барьерами памяти оглашается оконченным. Вследующей части мы примемся за операции compare-and-swap, их применение для оптимизации блокировок и важную роль вреализации неблокирующих связных списков.

Подробнее..

Перевод Rust 1.51.0 const generics MVP, новый распознаватель функциональности Cargo

26.03.2021 20:17:43 | Автор: admin

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


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.51.0 вам достаточно выполнить следующую команду:


rustup update stable

Если у вас ещё не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта, а также посмотреть подробные примечания к выпуску на GitHub.


Что было стабилизировано в 1.51.0


Этот выпуск представляет одно из наиболее крупных дополнений языка Rust и Cargo за долгое время, включающее в себя стабилизацию константных обобщений в минимально полезном варианте и новый распознаватель функциональности в Cargo. Давайте посмотрим подробнее!


Константные обобщения (Const Generics MVP)


До этого выпуска Rust позволял параметризовать типы по времени жизни или по типам. Например, если бы мы хотели получить struct, которая является обобщённой для массива, мы бы написали следующее:


struct FixedArray<T> {              // ^^^ Определение обобщённого типа.    list: [T; 32]        // ^ Где мы использовали его.}

Если затем мы используем FixedArray<u8>, компилятор создаст мономорфизированную версию FixedArray, которая выглядит так:


struct FixedArray<u8> {    list: [u8; 32]}

Этот полезный функционал позволяет писать повторно используемый код без дополнительных затрат во время выполнения. Однако до этого выпуска у нас не было возможности легко объединять значения таких типов. Это наиболее заметно в массивах, где длина указывается в определении типа ([T; N]). Теперь в версии 1.51.0 вы можете писать код, который будет обобщённым для значений любого числа, типа bool или char! (Использование значений struct и enum по-прежнему не стабилизировано.)


Это изменение позволяет создать массив, обобщённый по типу и длине. Давайте посмотрим на пример определения и то, как его можно использовать.


struct Array<T, const LENGTH: usize> {    //          ^^^^^^^^^^^^^^^^^^^ Определение константного обобщения.    list: [T; LENGTH]    //        ^^^^^^ Мы использовали его здесь.}

Теперь если мы используем Array<u8, 32>, компилятор создаст мономорфизированную версию Array, которая выглядит так:


struct Array<u8, 32> {    list: [u8; 32]}

Константные обобщения добавляют важный новый инструмент для разработчиков библиотек, чтобы создавать новые, мощные и безопасных API во время компиляции. Если вы хотите узнать больше о константных обобщениях, можете почитать статью в блоге Const Generics MVP Hits Beta для получения дополнительной информации об этой функции и её текущих ограничениях. Нам не терпится увидеть, какие новые библиотеки и API вы создадите!


Стабилизация array::IntoIter


Как часть стабилизации константных обобщений, мы также стабилизировали использующее их новое API std::array::IntoIter. IntoIter позволяет вам создать поверх массива итератор по значению. Ранее не было удобного способа итерироваться по самим значениям, только по ссылкам.


fn main() {  let array = [1, 2, 3, 4, 5];  // Раньше  for item in array.iter().copied() {      println!("{}", item);  }  // Теперь  for item in std::array::IntoIter::new(array) {      println!("{}", item);  }}

Обратите внимание, что это было добавлено вместо .into_iter() как отдельный метод, так как сейчас оно ломает текущее соглашение о том, что .into_iter() относится к срезам по ссылочному итератору. Мы изучаем возможности в будущем сделать такой код более эргономичным.


Новый распознаватель функциональности Cargo


Управление зависимостями сложная задача, и одна из самых сложных её частей выбор версии зависимости, когда от неё зависят два разных пакета. Здесь учитывается не только номер версии, но и то, какая функциональность была включена или выключена для пакета. По умолчанию Cargo объединяет функциональные флаги (features) для одного пакета, если он встречается в графе зависимостей несколько раз.


Например, у вас есть зависимость foo с функциональными флагами A и B, которые используются пакетами bar and baz, но bar зависит от foo+A, а baz от foo+B. Cargo объединит оба флага и соберёт foo как foo+AB. Выгода здесь в том, что foo будет собран только один раз и далее будет использован и для bar, и для baz.


Но у этого решения есть и обратная сторона. Что, если функциональный флаг подключён как build-зависимость, но не совместим с конечной целью сборки?


Общим примером этого из экосистемы может служить опциональная функциональность std во многих #![no_std] пакетах, которая позволяет этим пакетам предоставить дополнительную функциональность, если она включена. Теперь представим, что вы хотите использовать #![no_std] версию foo в вашей #![no_std] программе и использовать foo во время сборки в build.rs. Так как во время сборки вы зависите от foo+std, то и ваша программа тоже зависит от foo+std, а значит более не может быть скомпилирована, так как std не доступна для вашей целевой платформы.


Это была давняя проблема в Cargo, и с этим выпуском появилась новая опция resolver в вашем Cargo.toml, где вы можете установить resolver="2", чтобы попробовать новый подход к разрешению функциональных флагов. Вы можете ознакомиться с RFC 2957 для получения подробного описания поведения, которое можно резюмировать следующим образом.


  • Dev dependencies когда пакет используется совместно как обычная зависимость и dev, возможности dev-зависимости включаются только в том случае, если текущая сборка включает dev-зависимости.
  • Host Dependencies когда пакет совместно используется как обычная зависимость и зависимость сборки или процедурный макрос, features для нормальной зависимости сохраняются независимо от зависимости сборки или процедурного макроса.
  • Target dependencies когда у пакета включены зависимые от платформы features, и он присутствует в графе сборки несколько раз, будут включены только features, подходящие текущей платформе сборки.

Хотя это может привести к компиляции некоторых пакетов более одного раза, это должно обеспечить гораздо более интуитивный опыт разработки при использовании функций с Cargo. Если вы хотите узнать больше, вы также можете прочитать раздел Feature Resolver в Cargo Book для получения дополнительной информации. Мы хотели бы поблагодарить команду Cargo и всех участников за их тяжёлую работу по разработке и внедрению нового механизма!


[package]resolver = "2"# Или если вы используете workspace[workspace]resolver = "2"

Разделение отладочной информации


Хоть это и нечасто освещается в релизах, команда Rust постоянно работает над сокращением времени компиляции. В этом выпуске вносится самое крупное улучшение за долгое время для Rust на macOS. Отладочная информация исходного кода содержится в собранном бинарнике, и за счет этого программа может дать больше информации о том, что происходит во время исполнения. Раньше в macOS отладочная информация собиралась в единую директорию .dSYM при помощи утилиты dsymutil, что могло занимать много времени и дискового пространства.


Сбор всей отладочной информации в эту директорию помогал найти её во время выполнения, особенно если бинарник перемещался. Однако у такого решения есть и обратная сторона: если вы сделали небольшое изменение в вашей программе, то dsymutil необходимо запустить над всем собранным бинарником, чтобы собрать директорию .dSYM. Иногда это могло сильно увеличить время сборки, особенно для крупных проектов, поскольку надо перебирать все зависимости, но это важный шаг, без которого стандартная библиотека Rust не знает, как загружать отладочную информацию на macOS.


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


Вы можете включить новое поведение, установив флаг -Csplit-debuginfo=unpacked при запуске rustc или задав опцию split-debuginfo в unpacked раздела [profile] в Cargo. С опцией "unpacked" rustc будет оставлять объектные файлы (.o) в директории сборки вместо их удаления и пропустит запуск dsymutil. Поддержка бэктрейсов Rust достаточно умна, чтобы понять, как найти эти .o файлы. Такие инструменты, как lldb, также знают, как это делается. Это должно работать до тех пор, пока вам не понадобится переместить бинарники в другое место и сохранить отладочную информацию.


[profile.dev]split-debuginfo = "unpacked"

Стабилизированные API


Итого: в этом выпуске было стабилизировано 18 новых методов для разных типов, например slice и Peekable. Одним из примечательных дополнений является стабилизация ptr::addr_of! и ptr::addr_of_mut!, которая позволяет вам создавать сырые указатели для полей без выравнивания. Ранее это было невозможно, так как Rust требовал, чтобы &/&mut были выровнены и указывали на инициализированные данные. Из-за этого преобразование &addr as *const _ приводило к неопределённому поведению, так как &addr должно быть выровнено. Теперь эти два макроса позволяют вам безопасно создать невыровненные указатели.


use std::ptr;#[repr(packed)]struct Packed {    f1: u8,    f2: u16,}let packed = Packed { f1: 1, f2: 2 };// `&packed.f2` будет создана ссылка на невыровненную память, таким образом это неопределённое поведение!let raw_f2 = ptr::addr_of!(packed.f2);assert_eq!(unsafe { raw_f2.read_unaligned() }, 2);

Следующие методы были стабилизированы:



Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.51.0


Множество людей собрались вместе, чтобы создать Rust 1.51.0. Мы не смогли бы сделать это без всех вас. Спасибо!


От переводчиков


С любыми вопросами по языку Rust вам смогут помочь в русскоязычном Телеграм-чате или же в аналогичном чате для новичковых вопросов. Если у вас есть вопросы по переводам или хотите помогать с ними, то обращайтесь в чат переводчиков.
Также можете поддержать нас на OpenCollective.


Данную статью совместными усилиями перевели andreevlex, TelegaOvoshey, blandger, nlinker и funkill.

Подробнее..

Перевод Rust 1.52.0 улучшения Clippy и стабилизация API

07.05.2021 14:19:42 | Автор: admin

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


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.52.0 вам достаточно выполнить следующую команду:


rustup update stable

Если у вас ещё не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта, а также посмотреть подробные примечания к выпуску на GitHub.


Что было стабилизировано в 1.52.0


Самое значительное изменение этого выпуска не касается самого языка или стандартной библиотеки. Это улучшения в Clippy.


Ранее запуск cargo clippy после cargo check не запускал Clippy: кэширование в Cargo не видело разницы между ними. В версии 1.52 это поведение было исправлено, а значит, теперь пользователи будут получать то поведение, которое ожидают, независимо от порядка запуска этих команд.


Стабилизированные API


Следующие методы были стабилизированы:



Следующие ранее стабилизированные API стали const:



Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.52.0


Множество людей собрались вместе, чтобы создать Rust 1.52.0. Мы не смогли бы сделать это без всех вас. Спасибо!


От переводчиков


С любыми вопросами по языку Rust вам смогут помочь в русскоязычном Телеграм-чате или же в аналогичном чате для новичковых вопросов. Если у вас есть вопросы по переводам или хотите помогать с ними, то обращайтесь в чат переводчиков.
Также можете поддержать нас на OpenCollective.


Данную статью совместными усилиями перевели Belanchuk, TelegaOvoshey, blandger, nlinker и funkill.

Подробнее..

Перевод Rust 1.53.0 IntoIterator для массивов, quotquot в шаблонах, Unicode-идентификаторы, поддержка имени HEAD-ветки в Cargo

18.06.2021 18:20:53 | Автор: admin

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


Если вы установили предыдущую версию Rust средствами rustup, то для обновления до версии 1.53.0 вам достаточно выполнить следующую команду:


rustup update stable

Если у вас ещё не установлен rustup, вы можете установить его с соответствующей страницы нашего веб-сайта, а также посмотреть подробные примечания к выпуску на GitHub.


Что было стабилизировано в 1.53.0


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


IntoIterator для массивов


Это первый выпуск Rust, в котором массивы реализуют типаж IntoIterator. Теперь вы можете итерироваться в массиве по значению:


for i in [1, 2, 3] {    ..}

Раньше это было возможно только по ссылке, с помощью &[1, 2, 3] или [1, 2, 3].iter().


Аналогично вы теперь можете передать массив в методы, ожидающие T: IntoIterator:


let set = BTreeSet::from_iter([1, 2, 3]);

for (a, b) in some_iterator.chain([1]).zip([1, 2, 3]) {    ..}

Это не было реализовано ранее из-за проблем с совместимостью. IntoIterator всегда реализуется для ссылок на массивы и в предыдущих выпусках array.into_iter() компилировался, преобразовываясь в (&array).into_iter().


Начиная с этого выпуска, массивы реализуют IntoIterator с небольшими оговорками для устранения несовместимости кода. Компилятор, как и прежде, преобразовывает array.into_iter() в (&array).into_iter(), как если бы реализации типажа ещё не было. Это касается только синтаксиса вызова метода .into_iter() и не затрагивает, например, for e in [1, 2, 3], iter.zip([1, 2, 3]) или IntoIterator::into_iter([1, 2, 3]), которые прекрасно компилируются.


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


"Или" в шаблонах


Синтаксис шаблонов был расширен поддержкой |, вложенного в шаблон где угодно. Это позволяет писать Some(1 | 2) вместо Some(1) | Some(2).


match result {     Ok(Some(1 | 2)) => { .. }     Err(MyError { kind: FileNotFound | PermissionDenied, .. }) => { .. }     _ => { .. }}

Unicode-идентификаторы


Теперь идентификаторы могут содержать не-ASCII символы. Можно использовать все действительные идентификаторы символов Unicode, определённые в UAX #31. Туда включены символы из многих разных языков и письменностей но не эмодзи.


Например:


const BLHAJ: &str = "";struct  {    : String,}let  = 1;

Компилятор предупредит о потенциальной путанице с участием разных символов. Например, при использовании идентификаторов, которые выглядят очень похоже, мы получим предупреждение:


warning: identifier pair considered confusable between `` and `s`

Поддержка имени HEAD-ветки в Cargo


Cargo больше не предполагает, что HEAD-ветка в git-репозитории называется master. А следовательно, вам не надо указывать branch = "main" для зависимостей из git-репозиториев, в которых ветка по умолчанию main.


Инкрементальная компиляция до сих пор отключена по умолчанию


Как ранее говорилось в анонсе 1.52.1, инкрементальная компиляция была отключена для стабильных выпусков Rust. Функциональность остаётся доступной в каналах beta и nightly. Метод включения инкрементальной компиляции в 1.53.0 не изменился с 1.52.1.


Стабилизированные API


Следующие методы и реализации типажей были стабилизированы:



Другие изменения


Синтаксис, пакетный менеджер Cargo и анализатор Clippy также претерпели некоторые изменения.


Участники 1.53.0


Множество людей собрались вместе, чтобы создать Rust 1.53.0. Мы не смогли бы сделать это без всех вас. Спасибо!




От переводчиков


С любыми вопросами по языку Rust вам смогут помочь в русскоязычном Телеграм-чате или же в аналогичном чате для новичковых вопросов. Если у вас есть вопросы по переводам или хотите помогать с ними, то обращайтесь в чат переводчиков.
Также можете поддержать нас на OpenCollective.


Данную статью совместными усилиями перевели TelegaOvoshey, blandger, Belanchuk и funkill.

Подробнее..

Релиз мобильных приложений одной кнопкой

02.07.2020 18:23:23 | Автор: admin


Всем привет! Меня зовут Михаил Булгаков (нет, не родственник), я работаю релиз-инженером в Badoo. Пять лет назад я занялся автоматизацией релизов iOS-приложений, о чём подробно рассказывал в этой статье. А после взялся и за Android-приложения.

Сегодня я подведу некоторые итоги: расскажу, к чему мы пришли за это время. Long story short: любой причастный к процессу сотрудник может зарелизить хоть все наши приложения на обеих платформах в несколько кликов без головной боли, больших затрат времени, регистрации и СМС. Так, наш отдел релиз-инженеров за 2019 год сэкономил около 830 часов.

За подробностями добро пожаловать под кат!

Что стоит за мобильным релизом


Выпуск приложения в Badoo состоит из трёх этапов:

  1. Разработка.
  2. Подготовка витрины в магазине приложений: тексты, картинки всё то, что видит пользователь в App Store или Google Play.
  3. Релиз, которым занимается команда релиз-инжиниринга.

Когда приложение полностью готово и первый этап пройден, важно не налажать на этапе релиза и донести продукт до прилавка. Этот последний этап кажется самым простым, но по факту он занимает немало времени и его успех зависит от нескольких человек.

Большая часть времени уходит на подготовку витрины приложения в App Store или Google Play: необходимо залить красивые скриншоты, сделать завлекающее описание, оптимизированное для лучшей индексации, выбрать ключевые слова для поиска. От качества этой работы напрямую зависит популярность приложения, то есть по факту результат деятельности разработчиков, тестировщиков, дизайнеров, продакт-менеджеров, маркетологов всех причастных к созданию продукта.

Если приложение должно существовать на нескольких языках, для подготовки витрины нужен как минимум отдельный человек, а то и несколько сотрудников: менеджер продукта, который напишет тексты для описания, организует перевод на все языки и подготовит ТЗ для создания скриншотов, дизайнер, который нарисует скриншоты с наложенным текстом, контурами девайса и т. д., и, конечно же, переводчики, которые переведут все скриншоты и тексты на разные языки.

Заключительная часть работы сам процесс релиза. На него уходит значительное количество времени небольшой команды релиз-инжиниринга. На этом ответственном, но довольно рутинном этапе мы стремились минимизировать количество ошибок и влияние человеческого фактора. Для этого в первую очередь нужно было автоматизировать загрузку метаданных (текстового и графического оформления витрины приложения): это позволяет заметно сократить временные затраты и быстро осуществлять бизнес-релизы (например, стилизацию приложения ко Дню всех влюблённых).

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



Первые шаги на пути к автоматизации: загрузка метаданных


Как это работало в самом начале: для каждого релиза создавалась таблица в Google Sheets, в которую продакт-менеджер заливал выверенный мастер-текст на английском, после чего переводчики адаптировали его под конкретную страну, диалект и аудиторию, а затем релиз-инженер переносил всю информацию из этой таблицы в App Store или Google Play.

Первый шаг к автоматизации, который мы сделали, интегрировали перевод текстов в наш общий процесс переводов. Останавливаться на этом не буду это отдельная большая система, про которую можно прочитать в нашей недавней статье. Основной смысл в том, что переводчики не тратят время на таблички и работают с интерфейсом для удобной загрузки руками (читай: ctrl+c ctrl+v) переведённых вариантов в стор. Кроме того, присутствуют задатки версионирования и фундамент для Infrastructure-as-Code.

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

Какое-то время мы жили так, и в целом нас всё устраивало. Но количество приложений увеличивалось, а вместе с ним и время на подготовку каждого релиза.


Наша реальность по состоянию на 2015 год

В среднем на релиз одного приложения при наличии актуальной версии скриншотов уходило около полутора-двух часов работы релиз-инженера в случае с iOS и около получаса в случае с Android. Разница обусловлена тем, что iOS-приложения должны пройти так называемый Processing, который занимает некоторое время (отправить приложение на Review до успешного завершения Processing невозможно). Кроме того, App Store сам по себе по большинству операций в тот момент работал гораздо медленнее, чем Google Play.

Стало очевидно, что нам нужен дополнительный инструмент для доставки приложений в сторы. И как раз в тот момент на open-source-рынке начал набирать популярность продукт под названием Fastlane. Несмотря на то, что он тогда ещё был сыроватый, он уже мог решить огромный пласт наших проблем

Скажу о нём несколько слов, чтобы было понятнее, о чём пойдёт речь дальше.

Коротко о Fastlane


Сегодня Fastlane это продукт, который способен практически полностью автоматизировать все действия от момента окончания разработки до релиза приложения в App Store и Google Play. И речь не только о загрузке текстов, скриншотов и самого приложения здесь и управление сертификатами, и бета-тестирование, и подписывание кода, и многое другое.

Мы познакомились с Fastlane во времена его юности и нестабильности. Но сейчас это уверенно работающий и неотъемлемый компонент многих команд разработки мобильных приложений, которые сталкиваются с проблемой огромных временных затрат на доставку своих продуктов пользователям. Самое интересное в нём это возможности писать собственные плагины для основного компонента и пользоваться плагинами, написанными сообществом. Для такого специфического продукта это хорошее решение, которое (что важно) помогает не плодить лишние технологии в DevTools.

Доверие внушает и то, что основателя и главного разработчика Fastlane взяли на работу в Google: теперь компонент поддерживает не только комьюнити, но и Сам.

Со временем мы внедрили большинство предоставляемых Fastlane возможностей в системы сборки, подписания, заливки и т. д. наших приложений. И несказанно этому рады. Зачем изобретать колесо, да ещё и поддерживать его правильную форму, когда можно один раз написать унифицированный сценарий, который будет сам крутиться в CI/CD-системе?

Автоматизация iOS-релизов


По причине того, что Google Play более дружелюбен к разработчикам, на релиз Android-приложения уходило очень мало времени: без обновления текстов, видео и скриншотов пара минут. Отсюда и отсутствие необходимости в автоматизации. А вот с App Store проблема была очень даже осязаемой: слишком много времени уходило на отправку приложений на Review. Поэтому было решено начать автоматизацию именно с iOS.

Подобие своей системы автоматизации взаимодействия с App Store мы обдумывали (и даже сделали прототипы), но у нас не было ресурсов на допиливание и актуализацию. Также не было никакого мало-мальски адекватного API от Apple. Ну и последний гвоздь в гроб нашего кастомного решения вбили регулярные обновления App Store и его механизмов. В общем, мы решили попробовать Fastlane тогда ещё версии 2015 года.

Первым делом был написан механизм выгрузки переведённых текстов для приложений в нужную структуру как компонент нашей общей внутренней системы AIDA (Automated Interactive Deploy Assistant). Эта система своеобразный хаб, связующее звено между всеми системами, технологиями и компонентами, используемыми в Badoo. Работает она на самописной системе очередей, реализованной на Golang и MySQL. Поддерживает и совершенствует её в основном отдел Release Engineering. Подробнее о ней мы рассказывали в статье ещё в 2013 году, с тех пор многое изменилось. Обещаем рассказать про неё снова AIDA классная!

На следующем этапе выгруженные тексты скармливались Fastlane, который загружал их в App Store. После этого приходилось заходить в интерфейс App Store, вручную выбирать нужную загруженную версию и отправлять приложение на проверку, если к тому времени уже был завершён Processing.

Это сократило время подготовки релиза с пары часов до примерно 30 минут, из которых только полторы минуты надо было что-то делать руками! Остальное время ждать. Ждать окончания Processing. Механизм стал прорывом на тот момент как раз потому, что почти полностью избавил нас от ручной работы при подготовке AppStore к релизу. Под скрипт мы сделали репозиторий, к которому дали доступ людям, имеющим непосредственное отношение к релизам (проджект-менеджерам, релиз-инженерам).

В таком режиме мы жили ещё некоторое время. Но в какой-то момент эта схема привела к тому, что накопилось много сакральных знаний, обладателем которых и, как следствие, общей картины событий стал один-единственный человек, а это не есть хорошо. Особенно для самого этого человека: даже в отпуск без ноутбука не поедешь.

Кроме того, вокруг этого механизма было очень много разрозненных инфраструктурных компонентов, практически никак не связанных друг с другом.

  1. Нужно было идти в TeamCity за свежей сборкой, скачивать оттуда IPA-файл, загружать его в App Store через Application Manager.
  2. Потом идти в интерфейс с переводами в AIDA, смотреть, готовы ли все переводы, запускать скрипт, убеждаться, что он правильно сработал (всё-таки на тот момент Fastlane был ещё сыроват).
  3. После этого залезать в App Store и обновлять страницу с версией до того момента, пока не завершится Processing.
  4. И только после этого отправлять приложение на Review.

И так с каждым приложением. Напомню, на тот момент у нас их было восемь.

Следующим действием было решено перенести скрипт в нашу AIDA, заодно объединив и автоматизировав все шаги до момента отправки приложения: проверку на готовность переводов, сбор данных из TeamCity, оповещение, логирование и все остальные блага XXI века. Параллельно с этим мы начали загружать все собранные версии в TestFlight на этапе сборки.

TestFlight это приложение сторонних разработчиков, когда-то купленное Apple для тестирования готового приложения внешними тестировщиками практически в продакшен-окружении, то есть с push-оповещениями и вот этим всем.


AIDA молодец, будь как AIDA!

Всё это привело к сокращению времени с получаса до полутора минут на всё про всё: IPA-файл успевал пройти Processing ещё до того момента, когда команда QA-инженеров давала отмашку на запуск релиза. Тем не менее нам всё равно приходилось идти в App Store, выбирать нужную версию и отправлять её на Review.

Плюс, был нарисован простенький интерфейс: мы же все любим клац-клац.


Вот так, вкладка за вкладкой, Ctrl+C Ctrl+V...

Автоматизация Android-релизов


Далее встал вопрос об автоматизации релизов Android-приложений. Хотя этот процесс был значительно быстрее, но руками приходилось делать довольно много:

  1. Заходить в консоль Google Play, чтобы убедиться, что предыдущая версия раскатана на 100% пользователей или заморожена.
  2. Создавать новую версию релиза с обновлёнными текстами и скриншотами (при наличии).
  3. Загружать APK-файл (Android Package), загружать Mapping-файл.
  4. Идти в HockeyApp (использовался в то время для логирования крашей), загружать туда APK-файл и Mapping-файл.
  5. Идти в чат и отписываться о статусе релиза.

И так с каждым приложением.

Да, у Google Play есть свой API. Но зачем делать обёртку, следить за изменениями в протоколе, поддерживать её и плодить сущности без необходимости, если мы уже используем Fastlane для iOS-релизов? К тому же он комфортно существует на нашем сервере, варится в своём соку и вообще обновляется. А к тому времени он ещё и научился адекватно релизить Android-приложения. Звёзды сошлись!

Первым делом мы выпилили отовсюду всё старое, что было: отдельные скрипты, наброски автоматизации, старые обёртки для API это создавалось когда-то в качестве эксперимента и не представляло особой ценности. Сразу после этого мы добавили команду в AIDA, которая уже умела забирать что нужно из TeamCity, загружать что надо куда надо в HockeyApp, отправлять оповещения, логировать активность, и вообще она член команды.

Заливкой APK- и Mapping-файлов в Google Play занимался Fastlane. Надо сказать, что по проторенной тропе идти гораздо проще: реализовано это было достаточно быстро с минимальным количеством усилий.

На определённом этапе реализации автоматизации случился переход с APK-архивов на AAB (Android App Bundle). Опять же, нам повезло, что по горячим следам довольно быстро получилось всё поправить, но и развлечений добавилось в связи с этим переходом. Например, подгадил HockeyApp, который не умел использовать AAB-архивы в связи с подготовкой к самовыпиливанию. Так что для того чтобы комфортно продолжать его использовать, нужно было после сборки AAB разобрать собранный архив, доставать оттуда Mapping-файл, который полетит в HockeyApp, а из AAB нужно было отдельно собрать APK-файл и только потом загружать его в тот же HockeyApp. Звучит весело. При этом сам Google Play отлично раскладывает AAB, достаёт оттуда Mapping-файл и вставляет его куда нужно. Так что мы избавились от одного шага и добавили несколько, но от этого было никуда не деться.

Был написан интерфейс (опять же, по аналогии с iOS), который умел загружать новую версию, проверять релиз вдоль и поперёк, управлять текущим активным релизом (например, повышать rollout percentage). В таком виде мы отдали его ответственным за релизы членам команды Android QA, стали собирать фидбэк, исправлять недочёты, допиливать логику (и что там ещё бывает после релиза 1.0?).

Кстати, в дальнейшем автоматизация дала нам возможность заливать в Google Play бета-версии приложений автоматически по расписанию, что, в свою очередь, довольно сильно ускорило процесс автоматического и регрессионного тестирования.

Унификация флоу мобильных релизов


К моменту автоматизации Android-релизов Fastlane наконец-то научился отправлять версии iOS-приложений на ревью. А мы немного усовершенствовали систему проверки версий в AIDA.

Пришла пора отдать iOS-релизы на откуп команде QA-инженеров. Для этого мы решили нарисовать красивую формочку, которая бы полностью покрывала потребности, возникающие в процессе релиза iOS-приложений: давала бы возможность выбирать нужный билд в TeamCity по предопределённым параметрам, выбирать вариант загружаемых текстов, обновлять или нет опциональные поля (например, Promotional Text).

Сказано сделано. Формочка получилась очень симпатичная и полностью удовлетворяет все запросы. Более того, с её внедрением появилась возможность выбирать сразу все необходимые приложения со всеми требуемыми параметрами, так что и взаимодействие с интерфейсом свелось к минимуму. AIDA по команде присылает ссылку на build log, по которому можно отслеживать возникающие ошибки, убеждаться, что всё прошло хорошо, получать какую-то debug-информацию вроде версии загружаемого IPA-файла, версии релиза и т. д. Вот так красиво iOS-релизы и были переданы команде iOS QA.


Ну симпатично же?

Идея с формочкой понравилась нам настолько, что мы решили сделать аналогичную и для Android-релизов. Принимая во внимание то, что у нас есть приложение, полностью написанное на React Native, и что команда QA-инженеров этого приложения отвечает как за iOS-, так и за Android-релизы.

Уже используемый командой Android QA интерфейс был интегрирован с изменениями в аналогичную формочку, процесс был адаптирован под новые реалии всё было максимально приближено к процессам iOS-команды. Это дало стимул наконец набросать более-менее конечный вариант документации для обеих платформ (в процессе постоянных изменений делать это категорически не хотелось), а ещё отвязать процесс релиза от всех искусственных ограничений, которые сложились исторически и приводили к лишним телодвижениям в нестандартных ситуациях, требуя действий команды релиз-инженеров.

Вывод


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

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

Здорово, что сейчас есть возможность автоматизации большинства рутинных действий, что инженеры могут писать код когда хотят, а любой код могут поддерживать и сторонние разработчики, не тратя драгоценное время на копание в сложных интерфейсах. Особенно трудно бывает разобраться в моментах типа Где надо поставить галочку?, когда на часах полночь, в офисе никого нет, а hotfix надо залить здесь и сейчас.

Пять лет. Почему так долго? Во-первых, мобильные релизы далеко не единственная зона ответственности нашей небольшой команды. Во-вторых, конечно же, требовалось время на развитие нового open-source-проекта Fastlane; наша система релизов развивалась вместе с ним.

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

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru