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

Автор оригинала: The Rust Core Team
  • Перевод

Команда 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<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.

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 10

    +3
    > as будет выполнять «насыщающее приведение» (saturating cast),

    Не очень видно логики. В случае целочисленных действий, например, есть явные операции типа checked_add, overflowed_add, а есть просто "+", которое одно из них в зависимости от режима компиляции. Почему не точно так же с конверсией из плавающего?

    Переводить poison value как «испорчено» — странная манера… точно нет варианта лучше?
      –7
      Хреново когда нет исключений. Но можно же было просто остановить исполнение с помощью std::panic
        +3
        Ну паники это и есть аналог исключений для Rust (разница для данного вопроса неважна), и распаковка, если checked_add выдал None, даст панику, а можно и явно проверить. Тут ровно такое же можно было бы провернуть.
          +2

          Если вам нужно такое поведение — используйте TryFrom.

            +1
            Спасибо, я в курсе, и с ходу могу придумать ещё пару методов получить нужный результат. Но обидно за неконсистентность.
    –4

    Мне кажется, было бы лучше вообще запретить любые преобразования чисел с большим разрядом в числа с меньшим. Нужно 8 бит — значит, изначально возьми именно 8 бит, вместо того чтобы парсить 32 бита с риском получить на выходе какую-то фигню.

      +7

      Для целых чисел есть TryFrom/TryInto. Можно проверить, поместится ли число в целевой тип.
      Запрет на любые преобразования чисел с большим разрядом в младшие — драконовы меры.

      +9
      Последние годы чуть ли не самое интересное в обновлениях Rust, это «скучный» список в конце статьи с оглавлением «В Rust 1.*.0 были стабилизированы следующие функции».

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

      Мне кажется, что команде Rust пишущей анонсы стоит уделять большие внимание новым стабилизированным возможностям стандартной библиотеки, потому что для большинства разработчиков Rust – это новые возможности, а ребята как-будто до сих пор живут в предубеждении что каждый разработчик Rust хотя бы раз в жизни забутстрепил и собрал руками компилятор и знает наизусть весь список 439 еще не стабилизированых возможностей раста.

      Тот же BTreeMap::remove_entry как минимум мне несколько раз нужен был точно.
        0
        А версию 1.45.2, вышедшую почти неделю назад, 3 августа, так и не анонсировали.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое