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

?v=1

Команда 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 = 255
too_small_casted = 0
not_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,
        bar: RefCell,
    }

    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("//")]
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 были стабилизированы следующие функции:


  • Arc::as_ptr,
  • BTreeMap::remove_entry,
  • Rc::as_ptr,
  • rc::Weak::as_ptr,
  • rc::Weak::from_raw,
  • rc::Weak::into_raw,
  • sync::Weak::as_ptr,
  • sync::Weak::from_raw,
  • sync::Weak::into_raw,
  • str::strip_prefix,
  • str::strip_suffix,
  • char::UNICODE_VERSION,
  • Span::resolved_at,
  • Span::located_at,
  • Span::mixed_site,
  • unix::process::CommandExt::arg0.

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

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

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


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

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


Участники 1.45.0

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


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

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

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

© Habrahabr.ru