Rust 1.58.0: захватываемые идентификаторы, пути поиска в Windows, больше #[must_use] в стандартной библиотеке +21



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


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


rustup update stable

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


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


В Rust 1.58 появились захваченные идентификаторы в форматируемых строках, изменился путь поиска в Windows для Command, в стандартной библиотеке стало больше аннотаций #[must_use], а также были стабилизированы некоторые функции.


Захваченные идентификаторы в форматируемых строках


Форматируемые строки теперь могут захватывать аргументы, если вы просто напишете {ident} в строке. Форматируемые строки уже давно принимают позиционные аргументы (возможно, по индексу) и именованные аргументы. Например:


println!("Hello, {}!", get_person());                // implicit position
println!("Hello, {0}!", get_person());               // explicit index
println!("Hello, {person}!", person = get_person()); // named

Сейчас именованные аргументы также могут быть захвачены из окружения, например:


let person = get_person();
// ...
println!("Hello, {person}!"); // captures the local `person`

Это также может использоваться в параметрах форматирования:


let (width, precision) = get_format();
for (name, score) in get_scores() {
  println!("{name}: {score:width$.precision$}");
}

Форматируемые строки могут захватывать только простые идентификаторы, но не произвольные пути или выражения. Для более сложных аргументов либо сначала присвойте им локальное имя, либо используйте старый стиль форматирования аргументов name = expression.


Эта функция работает во всех макросах, принимающих форматируемые строки. Тем не менее, существует один крайний случай — работа макроса panic! в выпусках 2015 и 2018, где panic!("{ident}") по-прежнему обрабатывается как обычная строка. Компилятор предупредит об этом, но ожидаемого эффекта не произойдёт. Благодаря обновлению макроса паники в выпуске 2021 года для улучшения согласованности в panic! это будет работать ровно так, как и ожидалось.


Сокращение пути поиска для Command на Windows


На Windows std::process::Command больше не ищет исполняемые файлы в текущей директории. Этот эффект был связан с поведением win32 API CreateProcess, из-за чего поиск осуществлялся в следующем порядке:


  1. (Специфично для Rust) Директории перечисленные в переменной окружения PATH дочернего процесса, если эта переменная явно изменялась родительским процессом.
  2. Директория, из которой было загружено приложение
  3. Текущая директория для родительского процесса
  4. Системная директория 32-битной Windows
  5. Системная директория 16-битной Windows
  6. Директория Windows
  7. Директории, указанные в переменной окружения PATH

Однако использование текущего каталога может привести к неожиданным результатам или даже опасному поведению при работе с ненадёжными директориями. Например, ripgrep опубликовали CVE-2021-3013 когда узнали, что их дочерние процессы могут быть перехвачены таким образом. Даже собственные документы PowerShell Microsoft не используют текущую директорию ради безопасности.


Теперь Rust осуществляет свой поиск без учёта текущей директории и старой 16-битной директории, так как нет API, чтобы найти её расположение. Так что новый порядок поиска для Command на Windows таков:


  1. Директории, указанные в дочерней переменной окружения PATH
  2. Директория, из которой было загружено приложение
  3. Системная директория 32-битной Windows
  4. Директория Windows
  5. Директории, указанные в переменной окружения PATH

Не ориентированные на Windows приложения продолжат использовать прежнее специфическое для платформы поведение, чаще всего учитывая только дочернюю или родительскую переменную окружения PATH.


Больше атрибутов #[must_use] в стандартной библиотеке


Атрибут #[must_use] применяется к типам и функциям, у которых отсутствие явной обработки или результата считается ошибкой. Это давно используется в стандартной библиотеке для типов, подобных Result, которые должны быть проверены на наличие ошибок. Атрибут также помогает отловить ошибки ожидания изменения функцией передаваемого значения, в то время как она возвращает новое значение.


Библиотечное предложение №35 было одобрено для проверки в октябре 2021 года и расширяет применение #[must_use] в стандартной библиотеке. Оно покрывает больше функций, основной эффект которых — возвращение значения. Похоже на идею чистоты функций, но более слабо, чем настоящая языковая черта. Часть функций была представлена в 1.57.0 — теперь же добавлена оставшаяся часть.


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


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



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



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


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


Участники 1.58.0


Множество людей объединились для создания Rust 1.58.0. Мы не смогли бы сделать это без всех вас. Спасибо!


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


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


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




Комментарии (42):

  1. tbl
    /#23941753

    Смотрю на захват переменных, вспоминаю log4shell и думаю: "Надеюсь, парсинга шаблонов {...} в рантайме и исполнения инъектированного кода в планах нет?"

    • rmuskovets
      /#23941783 / +8

      В Rust println! и ему подобные принимают как шаблон только строковые литералы, такое кинет ошибку компиляции:

      let fmt = "{fmt}";
      println!(fmt);

      • tbl
        /#23942283

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

        • ZyXI
          /#23942433 / +4

          В рантайм протащить это было бы сложновато. Как технически, так и организационно. Во‐первых, у Rust в runtime просто нет такой кучи информации, которую можно получить в той же Java, если её туда специально не добавит компилятор. Я примерно представляю, как это «специально добавит» должно выглядеть и оно содержит множество принципиальных проблем и вообще непросто в реализации.


          Во‐вторых, пока даже на простенькие форматные строки с произвольными выражениями RFC не собрали — а там из проблем по сути только «как экранировать» и «а оно точно нужно?». Предложение вида «давайте потратим несколько человекомесяцев, чтобы получить в std свой собственный log4shell», конечно же найдёт широкую поддержку у разработчиков и сообщества.


          Более ограниченный вариант вида «пользователь должен сам собрать словарь со всеми переменными, которые он хочет сделать доступными» ещё может появится в std и наверняка в каком‐то виде уже есть в крейтах. Но это не особо опасно.

  2. amarao
    /#23941903

    Мне очень жалко, что фичу завезли только в макросах и в очень куцем виде. Я бы очень, очень хотел видеть полноценные f-строки.

    • Medeyko
      /#23941923

      А так уж ли это много даст? Это не так уж часто нужно, по-моему, а format! должен бы справиться во всех ситуациях, когда хочется f-строк, если я правильно понимаю.

    • Amomum
      /#23941989 / +1

      А разве это вообще возможно в компилируемом языке?

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

      • amarao
        /#23942075 / +7

        Мне кажется, вполне возможно.

        Если я могу написать так:

        [
          (expr1).to_string(),
          (expr2).to_sting(),
        ].join('')

        То и компилятор может развернуть f-string в что-то подобное. Это же просто синтаксический сахар.

        ... а, я понял вопрос. Нет, речь не про eval для произвольных строк. Речь про поддержку f-нотации с строковыми литералами.

        Чтобы можно было так: `f"Hello {username}! Now is {time.time()}!"`

        • Amomum
          /#23942077

          Ну, в компайл-тайм - да.

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

          • amarao
            /#23942081

            Вот я бы хотел тут больше сахара, чтобы без макросов, а на уровне языка. В конце-концов, нам же насыпали сахара в районе for, который на Iter и IntoIter завязан.

            • Amomum
              /#23942085

              А чем макрос плох? Они в расте достаточно гибкие, чтобы сделать почти все что угодно. А как обкатают фичу и если нужда будет - можно и в язык запилить, как с try! было

              • amarao
                /#23942111

                Много букв. Чем плох BEGIN END в языке?

                FOR x IN range(0, 10).into_iter()
                BEGIN
                
                   PRINT(FORMAT!("{}", x));
                
                END

                Нравится?

                • Amomum
                  /#23942129 / +2

                  Но позвольте, тут в итоге больше на один восклицательный знак..

                  println!("Hello, {person}!"); - это же сам println делает, будь это встроено в язык - что бы поменялось?

                  • amarao
                    /#23942137

                    `println!("Hello, {person}! Now is {time.time()}");` не скомпилируется. Только чистый захват, без выражений. Что сделает условный {person.to_lower()} и т.д. недоступным.

                    • Amomum
                      /#23942147 / +2

                      Но это пока, вроде как ничего не мешает - концептуально - этому работать точно так же в макросе. Просто пока не сделали.

                      Допилить макрос, мне кажется, существенно проще, чем допилить компилятор.

                      • mayorovp
                        /#23949605

                        Вот только этот макрос именно что реализуется компилятором:


                            #[rustc_builtin_macro]
                            #[macro_export]
                            macro_rules! format_args_nl {
                                ($fmt:expr) => {{ /* compiler built-in */ }};
                                ($fmt:expr, $($args:tt)*) => {{ /* compiler built-in */ }};
                            }

            • Medeyko
              /#23942173

              Почитал немного - в принципе, рассматривается и введение f-строк, причём скорее всего они будут просто синтаксическим сахаром к макросу format!

              Ну, в принципе, конечно, сахарнее, но это не настолько большая разница, как с for'ом, где этот сахар реально кардинально меняет степень читаемости и довольно част.

          • tbl
            /#23942339

            GraalVM даже при компиляции в нативный код тащит с собой jit-компилятор байт-кода, чтобы в рантайме оптимизировать то, что напрофилировал, плюс то, что при кодогенерации подъехало в classpath. А возможности для кодогенерации там обширные: агенты, ASM + CGLIB, LambdaMetaFactory и т.п.

          • horror_x
            /#23942479

            Прост я не припомню нигде в компилируемых языках ничего подобного

            C# же как минимум.

            • Cerberuser
              /#23942785

              А если компилируемые в нативный код? C# всё-таки AOT-компилируется только в байткод, насколько я помню.

              • horror_x
                /#23944269 / +1

                А какая разница? Что бы изменилось от компиляции в нейтив (тем более что при желании можно и так)?

              • mayorovp
                /#23949619

                Вот как раз AOT-компиляция идёт в машинный код, в отличии от JIT-компиляции.

            • Amomum
              /#23944169

              оО А как там это работает?

              • horror_x
                /#23944263

                Из документации:

                // Composite formatting:
                Console.WriteLine("Hello, {0}! Today is {1}, it's {2:HH:mm} now.", name, date.DayOfWeek, date);
                // String interpolation:
                Console.WriteLine($"Hello, {name}! Today is {date.DayOfWeek}, it's {date:HH:mm} now.");

                If an interpolated string has the type string, it's typically transformed into a String.Format method call. The compiler may replace String.Format with String.Concat if the analyzed behavior would be equivalent to concatenation.

                If an interpolated string has the type IFormattable or FormattableString, the compiler generates a call to the FormattableStringFactory.Create method.

                • Amomum
                  /#23945391

                  Интересно.. т.е. это преобразование заменяет имена переменных в строке на позиционные аргументы для String.Format?

                  • NN1
                    /#23945501

                    Нет.

                    Это в итоге вызовет WriteLine(string) или WriteLine(FormattableString) или другие варианты.

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

                    А само форматированние строки претерпело уже несколько реинкарнаций: https://habr.com/ru/company/skillfactory/blog/599341/

                    • Amomum
                      /#23945505

                      Ну так а как значения-то подставлены будут?

                      • NN1
                        /#23945511

                        Посмотрите статью, там всё сложно:)

                        Зависит от того, в какой тип попадает строка с интерполяцией.

                        Скажем в простом варианте:

                        string a="x", b = "y";

                        string c = $"{x}{y}";

                        Будет просто string.Concat(x,y)

                        В других вариантах будет посложнее.

                      • Amomum
                        /#23945517

                        Ага, ну то есть строка в компайл-тайм таки парсится? Только это делает особая компиляторная магия, специально сделанная для этого случая, а не макрос?

                      • AnthonyMikh
                        /#23947485 / +1

                        Технически ничто не мешает реализовать эти макросы
                        как библиотечные, просто выигрыш неочевиден. А так есть, скажем, defmt.

                      • NN1
                        /#23947825

                        Как же неочевиден.

                        Можно обновлять реализацию без изменения самого компилятора.

                        В идеале всё, что можно вынести в макросы в виде библиотек, а компилятор не менять.

                      • DarkEld3r
                        /#23950313

                        Не очень в теме, но в расте стандартную библиотеку и компилятор не одни и те же люди делают?.. Версионируется оно точно вместе. В целом я согласен, что библиотечная реализация лучше, но в данном конкретном случае разница не кажется принципиальной. Плюс есть подозрения, что такая "магическая" реализация — это последствия поздней стабилизации макросов, когда раст 1.0 уже был, а свой format! написать ещё было нельзя.

                      • AnthonyMikh
                        /#23951781

                        Плюс есть подозрения, что такая "магическая" реализация — это последствия поздней стабилизации макросов, когда раст 1.0 уже был, а свой format! написать ещё было нельзя.

                        Скорее всего.


                        Вдобавок, вынос этого макроса в либу — это +1 процедурный макрос со всеми вытекающими.

                      • DarkEld3r
                        /#23952501 / +2

                        Вдобавок, вынос этого макроса в либу — это +1 процедурный макрос со всеми вытекающими.

                        Честно говоря, не понял о каких последствиях речь. Реализация ведь и так есть, просто спрятана внутри компилятора, а не стандартной библиотеки. Если её перенести, то почему станет хуже?

                  • horror_x
                    /#23945513

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

                    В простейшем случае $"{foo} {bar}" будет заменено чем-то вроде String.Concat(foo, " ", bar).

        • cpud36
          /#23942477

          Там дело в том, что format обходится без преобразования в строку. Он только берёт ссылки на переменные(dyn Display и прочие), а потому возникают вопросы из серии: где разместить результаты, чтобы оно не рассыпалось в любой нетривиальной ситуации.

          Хотя, конечно, по большей части, там пока недоговорились о конкретном синтаксисе таких захватов(а-ля "а можно ли использовать строковые литералы и макросы?")

  3. mayorovp
    /#23949573

    Библиотечное предложение №35 было одобрено для проверки в октябре 2021 года и расширяет применение #[must_use] в стандартной библиотеке. Оно покрывает больше функций, основной эффект которых — возвращение значения. Похоже на идею чистоты функций, но более слабо, чем настоящая языковая черта.

    Нет, ничуть не похоже на идею чистоты функций.


    На самом деле это похоже на линейные типы, особенно есть ещё и трейт Copy не добавлять.

    • DarkEld3r
      /#23950323 / +1

      Предположу, что логика такая: если на результате функции висит #[must_use], то она (скорее всего) чистая. Но да, сравнение слегка странное.