Rust 1.82.0: cargo info, AArch64 для macOS в tier1, use<..>, небезопасные указатели и другое

от автора

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

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

$ rustup update stable

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

Если вы хотите помочь нам протестировать будущие выпуски, вы можете использовать канал beta (rustup default beta) или nightly (rustup default nightly). Пожалуйста, сообщайте обо всех встреченных вами ошибках.

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

cargo info

Теперь у Cargo есть подкоманда info для вывода информации о пакете в реестре. Не прошло и десяти лет, как мы выполнили вашу просьбу! За прошедшие годы было написано несколько сторонних расширений, но эта реализация разрабатывалась в рамках cargo-information прежде чем стала частью Cargo.

Как пример, ниже вы видите вывод cargo info cc:

cc #build-dependencies A build-time dependency for Cargo build scripts to assist in invoking the native C compiler to compile native C code into a static archive to be linked into Rust code. version: 1.1.23 (latest 1.1.30) license: MIT OR Apache-2.0 rust-version: 1.63 documentation: https://docs.rs/cc homepage: https://github.com/rust-lang/cc-rs repository: https://github.com/rust-lang/cc-rs crates.io: https://crates.io/crates/cc/1.1.23 features:   jobserver = []   parallel  = [dep:libc, dep:jobserver] note: to see how you depend on cc, run cargo tree --invert --package cc@1.1.23

По умолчанию cargo info описывает версию пакета из локального Cargo.lock, если таковой имеется. Также вы можете увидеть и более новую версию cargo info cc@1.1.30.

Изменение целевых платформ Apple

MacOS на 64-битном ARM теперь на уровне 1

Платформа aarch64-apple-darwin для macOS на 64-битном ARM (семейство чипов M1 и более молодые Apple silicon CPU) теперь на первом уровне поддержки, что означает высочайшие гарантии правильной работы. Как описано на странице поддержки платформ, каждое изменение в репозитории Rust должно пройти все тесты на каждой платформе первого уровня, прежде чем оно может быть принято. Эта платформа была добавлена на второй уровень поддержки в Rust 1.49, благодаря чему она появилась в rustup, а текущее изменение ставит её в один ряд с Linux на ARM64 и macOS, Linux или Windows на x86.

Платформа Mac Catalyst теперь на уровне 2

Mac Catalyst — это технология Apple, которая позволяет запускать iOS-приложения нативно на Mac. Это очень удобно при тестировании iOS-специфичного кода. По большей части за счёт этого cargo test --target=aarch64-apple-ios-macabi --target=x86_64-apple-ios-macabi просто работает (в отличие от обычных целевых платформ iOS, которые должны быть связаны в пакет сторонними инструментами до того, как будут запущены на устройстве или в симуляторе).

Эта целевая платформа теперь на втором уровне и может быть загружена при помощи rustup target add aarch64-apple-ios-macabi x86_64-apple-ios-macabi, так что сейчас самое время обновить ваши пайплайны, чтобы тестировать код и в iOS-подобном окружении.

Точный захват в синтаксисе use<..>

Rust теперь поддерживает синтаксис use <..> в пределах конкретных границ impl Trait для управления тем, какие общие параметры времени жизни будут захвачены.

Типы в возвращаемом значении impl Trait (RPIT) в Rust захватывают определённые общие параметры. Захват этих параметров позволяет использовать их в скрытых типах, что влияет на анализатор заимствований.

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

//@ edition: 2021 fn f(x: &()) -> impl Sized { x }

error[E0700]: hidden type for `impl Sized` captures lifetime that does not appear in bounds  --> src/main.rs:1:30   | 1 | fn f(x: &()) -> impl Sized { x }   |         ---     ----------   ^   |         |       |   |         |       opaque type defined here   |         hidden type `&()` captures the anonymous lifetime defined here   | help: add a `use<...>` bound to explicitly capture `'_`   | 1 | fn f(x: &()) -> impl Sized + use<'_> { x }   |                            +++++++++

С новым синтаксисом use<..> мы можем воспользоваться подсказкой в описании ошибки и исправить код следующим образом:

fn f(x: &()) -> impl Sized + use<'_> { x }

Ранее для корректного исправления этого класса ошибок требовалось определить фиктивный трейт, условно называемый Captures, и использовать его:

trait Captures<T: ?Sized> {} impl<T: ?Sized, U: ?Sized> Captures<T> for U {}  fn f(x: &()) -> impl Sized + Captures<&'_ ()> { x }

Это называлось "трюк с Captures«, и теперь в этом больше нет необходимости.

Также существовал менее корректный, но более удобный способ исправить ошибку. Вы могли его встретить под названием «трюк с выживанием». Ранее компилятор даже предлагал сделать это по умолчанию. Суть трюка заключалась в следующем:

fn f(x: &()) -> impl Sized + '_ { x }

В этом простом случае написанное в точности эквивалентно + use<'_> по причинам, описанным в RFC 3498. Однако в реальных случаях это приводит к чрезмерному ограничению возвращаемого непрозрачного типа, что приводит к проблемам. Например, рассмотрим код, который основан на реальном примере:

struct Ctx<'cx>(&'cx u8);  fn f<'cx, 'a>(     cx: Ctx<'cx>,     x: &'a u8, ) -> impl Iterator<Item = &'a u8> + 'cx {     core::iter::once_with(move || {         eprintln!("LOG: {}", cx.0);         x     }) //~^ ERROR lifetime may not live long enough }

Мы не можем удалить + 'cx, поскольку время жизни используется в скрытом типе и поэтому должно быть зафиксировано. Мы также не можем добавить ограничение для 'a: 'cx, поскольку эти периоды жизни на самом деле не связаны, и в целом неверно, что 'a переживает 'cx. Однако если мы напишем вместо этого + use<'cx, 'a>, то всё сработает как надо, и границы будут корректны.

Для того, что мы стабилизируем сегодня, есть некоторые ограничения. Синтаксис use<..> в настоящее время не может отображаться в трейтах или в имплементации трейтов (но обратите внимание, что там параметры времени жизни в области видимости уже записаны по умолчанию), и в нём должны быть перечислены все параметры общего типа и const в области видимости. Мы надеемся, что со временем эти ограничения получится снять.

Обратите внимание, что в Rust 2024 приведённые выше примеры будут «просто работать» без необходимости использования синтаксиса use<..> (или каких-либо хитростей). Это связано с тем, что в новой редакции непрозрачные типы будут автоматически фиксировать все параметры времени жизни в области видимости. Это лучший вариант по умолчанию, и мы видели много свидетельств того, как это делает код чище. В Rust 2024 синтаксис use<..> будет служить важным элементом отказа от этого значения по умолчанию.

Для получения более подробной информации о синтаксисе use<..>, захвате и о том, как это применимо к Rust 2024, смотрите главу RPITT lifetime capture rules руководства по редакциям. Подробнее об общем направлении работы смотрите в нашем недавнем посте в блоге — Changes to impl Trait in Rust 2024.

Собственный синтаксис для создания сырого указателя

Иногда небезопасный код имеет дело с указателями, которые могут быть висящими, оказаться смещены или указывать на некорректные данные. Чаще всего это встречается в структурах с repr(packed). В таком случае важно избегать создания ссылки, так как это может привести к неопределённому поведению. Это означает, что обычные операторы & и &mut не могут быть использованы, так как они создают ссылку. Даже если ссылка немедленно преобразуется в сырой указатель, неопределённого поведения всё равно не получится избежать.

Многие годы для этих целей использовались макросы std::ptr::addr_of! и std::ptr::addr_of_mut!. Теперь же настало время представить нативный синтаксис для этих операций: addr_of!(expr) стал &raw const expr, а addr_of_mut!(expr)&raw mut expr. Например:

#[repr(packed)] struct Packed {     not_aligned_field: i32, }  fn main() {     let p = Packed { not_aligned_field: 1_82 };      // Это должно привести к неопределённому поведению!     // Такая запись будет отклонена компилятором     //let ptr = &p.not_aligned_field as *const i32;      // Это старый вариант создания указателя     let ptr = std::ptr::addr_of!(p.not_aligned_field);      // А это — новый     let ptr = &raw const p.not_aligned_field;      // Обращение к указателю не изменилось     // Обратите внимание, что `val = *ptr` будет неопределённым поведением,     // так как указатель не выровнен!     let val = unsafe { ptr.read_unaligned() }; }

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

Безопасный код в unsafe extern

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

Однако если сигнатура в блоке extern неверна, то использование этого элемента приведёт к неопределённому поведению. Будет ли это ошибкой человека, который написал extern блок, или человека, который использовал этот элемент?

Мы решили, что человек, пишущий блок extern, несёт ответственность за то, чтобы все сигнатуры, содержащиеся в нём, были правильными, и поэтому теперь мы разрешаем писать unsafe extern:

unsafe extern {     pub safe static TAU: f64;     pub safe fn sqrt(x: f64) -> f64;     pub unsafe fn strlen(p: *const u8) -> usize; }

Одним из преимуществ такого подхода является то, что элементы в блоке unsafe extern могут быть помечены как безопасные для вызова. В приведённом выше примере мы можем вызвать sqrt без использования unsafe. Элементы, которые не помечены ни как safe, ни как unsafe, обычно считаются unsafe.

В будущих выпусках мы будем поощрять использование unsafe extern со статическими анализаторами кода. Начиная с Rust 2024, использование unsafe extern станет обязательным.

Более подробную информацию см. в RFC 3484 и главе «Небезопасные extern блоки» руководства по изданию.

Небезопасные атрибуты

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

#[unsafe(no_mangle)] pub fn my_global_function() { }

Старая форма атрибута (без unsafe) в настоящее время всё ещё принимается, но может быть подвергнута проверке в будущем и станет серьёзной ошибкой в Rust 2024.

Это влияет на следующие атрибуты:

  • no_mangle
  • link_section
  • export_name

Более подробную информацию см. в главе «Небезопасные атрибуты» руководства по изданию.

Исключение пустых типов при сопоставлении с образцом

Шаблоны, соответствующие пустым (т.е. необитаемым) типам по значению, теперь можно опускать:

use std::convert::Infallible; pub fn unwrap_without_panic<T>(x: Result<T, Infallible>) -> T {     let Ok(x) = x; // `Err` вариант описывать не требуется     x }

Это работает с пустыми типами, такими как enum Void {}, или со структурами и перечислениями с видимым пустым полем и без атрибута #[non_exhaustive]. Это также будет особенно полезно в сочетании с типом never !, хотя этот тип в настоящее время ещё нестабилен.

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

pub fn unwrap_ref_without_panic<T>(x: &Result<T, Infallible>) -> &T {     match x {         Ok(x) => x,         // эта ветвь не может быть исключена, потому что ссылка         Err(infallible) => match *infallible {},     } }

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

NaN и const

Операции над числами с плавающей точкой (f32 и f64) известны своей тонкостью. Одной из причин этого является существование NaN. NaN — это сокращение от «Not a Number» («не число»), оно используется, например, для представления результата 0.0 / 0.0. Тонкой работу с NaN делает тот факт, что существует более одного возможного значения NaN: значение NaN имеет знак, который можно проверить с помощью f.is_sign_positive(), а также «полезную нагрузку», которую можно извлечь с помощью f.to_bits(). Оба они, однако, полностью игнорируются == (который всегда возвращает false для NaN). Несмотря на весьма успешные попытки стандартизировать поведение операций с плавающей точкой в различных аппаратных архитектурах, детали того, является NaN положительным или отрицательным и какова его точная полезная нагрузка, различаются в разных архитектурах. Чтобы ещё больше усложнить задачу, Rust и его серверная часть LLVM применяют оптимизацию к операциям с плавающей точкой, когда точный числовой результат гарантированно не изменится, но эти оптимизации могут изменить получаемое значение NaN. Например, f * 1.0 может быть оптимизирован только для f. Однако если f является NaN, оптимизация может изменить точный битовый набор результата!

Начиная с этого выпуска Rust стандартизирует набор правил поведения значений NaN. Этот набор не является полностью детерминированным, и это означает, что результат операций, подобных (0.0 / 0.0).is_sign_positive(), может отличаться в зависимости от аппаратной архитектуры, уровней оптимизации и окружающего кода. Код, который задуман быть полностью переносимым, обязан избегать использования to_bits и должен использовать f.signum() == 1.0 вместо f.is_sign_positive(). Однако правила были тщательно подобраны, чтобы по-прежнему позволять использовать в Rust-коде дополнительные методы представления данных, такие как упаковка NaN. Для получения более подробной информации о том, каковы точные правила, ознакомьтесь с документацией.

Поскольку семантика значений NaN установлена, в этом выпуске также разрешено использование операций с плавающей точкой в const fn. По причинам, описанным выше, такие операции, как (0.0 / 0.0).is_sign_positive(), могут привести к другому результату при выполнении во время компиляции и во время выполнения. Это не ошибка, и код не должен полагаться на то, что const fn всегда приводит к одному и тому же результату.

Константы как непосредственные элементы сборки

Операнд сборки const теперь предоставляет способ использовать целые числа как непосредственные без предварительного сохранения их в регистре. В качестве примера мы реализуем системный вызов для write вручную:

const WRITE_SYSCALL: c_int = 0x01; // syscall 1 is `write` const STDOUT_HANDLE: c_int = 0x01; // `stdout` has file handle 1 const MSG: &str = "Hello, world!\n";  let written: usize;  // Сигнатура: `ssize_t write(int fd, const void buf[], size_t count)` unsafe {     core::arch::asm!(         "mov rax, {SYSCALL} // rax содержит номер системного вызова",         "mov rdi, {OUTPUT}  // rdi — `fd` (первый аргумент)",         "mov rdx, {LEN}     // rdx — `count` (третий аргумент)",         "syscall            // вызов системного вызова",         "mov {written}, rax // сохранение возвращаемого значения",         SYSCALL = const WRITE_SYSCALL,         OUTPUT = const STDOUT_HANDLE,         LEN = const MSG.len(),         in("rsi") MSG.as_ptr(), // rsi - `buf *` (второй аргумент)         written = out(reg) written,     ); }  assert_eq!(written, MSG.len());

Вывод:

Hello, world!

Ссылка на песочницу.

В приведённом выше примере оператор LEN = const MSG.len() заполняет спецификатор формата LEN с помощью непосредственного значения, которое принимает значение MSG.len(). Это можно увидеть в сгенерированной сборке (значение равно 14):

lea     rsi, [rip + .L__unnamed_3] mov     rax, 1    # rax holds the syscall number mov     rdi, 1    # rdi is `fd` (first argument) mov     rdx, 14   # rdx is `count` (third argument) syscall # invoke the syscall mov     rax, rax  # save the return value

Более подробную информацию смотрите по ссылке.

Безопасное разыменование unsafe static

Этот код теперь разрешён:

static mut STATIC_MUT: Type = Type::new(); extern "C" {     static EXTERN_STATIC: Type; } fn main() {      let static_mut_ptr = &raw mut STATIC_MUT;      let extern_static_ptr = &raw const EXTERN_STATIC; }

В контексте выражения STATIC_MUT и EXTERN_STATIC являются выражениями места. Ранее проверки безопасности компилятора не знали, что оператор raw ref на самом деле не влияет на место операнда, и рассматривали его как возможное чтение или запись в указатель. Однако на самом деле никакой опасности нет, поскольку он просто создаёт указатель.

Если это опустить, могут возникнуть проблемы, когда некоторые небезопасные блоки сообщаются как неиспользуемые, если вы отклоняете анализатор unused_unsafe. Теперь это может быть полезно только в старых версиях. Старайтесь аннотировать эти небезопасные блоки с помощью #[allow(unused_unsafe)], если хотите поддерживать несколько версий Rust, как в этом примере:

 static mut STATIC_MUT: Type = Type::new();  fn main() { +    #[allow(unused_unsafe)]      let static_mut_ptr = unsafe { std::ptr::addr_of_mut!(STATIC_MUT) };  }

Ожидается, что будущая версия Rust распространит это на другие выражения, чтобы они были безопасны не только в static.

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

Следующие API теперь можно использовать в контексте const:

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

Проверьте всё, что изменилось в Rust, Cargo и Clippy.

Кто работал над 1.82.0

Многие люди собрались вместе, чтобы создать Rust 1.82.0. Без вас мы бы не справились. Спасибо!

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

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

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


ссылка на оригинал статьи https://habr.com/ru/articles/851800/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *