Команда разработчиков Rust рада сообщить о выпуске новой версии Rust: 1.30.0. Rust — это системный язык программирования, нацеленный на безопасность, скорость и параллельное выполнение кода.


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


$ rustup update stable

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


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


Rust 1.30 — выдающийся выпуск с рядом важных нововведений. Но уже в понедельник в официальном блоге будет опубликована просьба проверить бета-версию Rust 1.31, которая станет первым релизом "Rust 2018". Дополнительную информацию об этом вы найдете в нашей предыдущей публикации "What is Rust 2018".


Процедурные макросы


Еще в Rust 1.15 мы добавили возможность определять "пользовательские derive-макросы". Например, с помощью serde_derive, вы можете объявить:


#[derive(Serialize, Deserialize, Debug)]
struct Pet {
    name: String,
}

И конвертировать Pet в JSON и обратно в структуру, используя serde_json. Это возможно благодаря автоматическому выводу типажей Serialize и Deserialize с помощью процедурных макросов в serde_derive.


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


Атрибутные макросы подобны derive-макросам для автоматического вывода, но вместо генерации кода только для атрибута #[derive], они позволяют пользователям создавать собственные новые атрибуты. Это делает их более гибкими: derive-макросы работают только для структур и перечислений, но атрибуты могут применяться и к другим объектам, таким как функции. Например, атрибутные макросы позволят вам при использовании веб-фреймворка делать следующее:


#[route(GET, "/")]
fn index() {

Этот атрибут #[route] будет определен в самом фреймворке как процедурный макрос. Его сигнатура будет выглядеть так:


#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {

Здесь у нас имеется два входных параметра типа TokenStream: первый — для содержимого самого атрибута, то есть это параметры GET, "/". Второй — это тело того объекта, к которому применен атрибут. В нашем случае — это fn index() {} и остальная часть тела функции.


Функциональные макросы определяют такие макросы, использование которых выглядит как вызов функции. Например, макрос sql!:


let sql = sql!(SELECT * FROM posts WHERE id=1);

Этот макрос внутри себя будет разбирать SQL-выражения и проверять их на синтаксическую корректность. Подобный макрос должен быть объявлен следующим образом:


#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {

Это похоже на сигнатуру derive-макроса: мы получаем токены, которые находятся внутри скобок, и возвращаем сгенерированный по ним код.


Макросы и use


Теперь можно импортировать макросы в область видимости с помощью ключевого слова use. Например, для использования макроса json из пакета serde-json, раньше использовалась запись:


#[macro_use]
extern crate serde_json;

let john = json!({
    "name": "John Doe",
    "age": 43,
    "phones": [
        "+44 1234567",
        "+44 2345678"
    ]
});

А теперь вы должны будете написать:


extern crate serde_json;

use serde_json::json;

let john = json!({
    "name": "John Doe",
    "age": 43,
    "phones": [
        "+44 1234567",
        "+44 2345678"
    ]
});

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


Наконец, стабилизирован пакет proc_macro, который дает API, необходимый для написания процедурных макросов. В нем также значительно улучшили API для обработки ошибок, и такие пакеты, как syn и quote уже используют его. Например, раньше:


#[derive(Serialize)]
struct Demo {
    ok: String,
    bad: std::thread::Thread,
}

приводило к такой ошибке:


error[E0277]: the trait bound `std::thread::Thread: _IMPL_SERIALIZE_FOR_Demo::_serde::Serialize` is not satisfied
 --> src/main.rs:3:10
  |
3 | #[derive(Serialize)]
  |          ^^^^^^^^^ the trait `_IMPL_SERIALIZE_FOR_Demo::_serde::Serialize` is not implemented for `std::thread::Thread`

Теперь же будет выдано:


error[E0277]: the trait bound `std::thread::Thread: serde::Serialize` is not satisfied
 --> src/main.rs:7:5
  |
7 |     bad: std::thread::Thread,
  |     ^^^ the trait `serde::Serialize` is not implemented for `std::thread::Thread`

Улучшение системы модулей


Система модулей долгое время становилась больным местом для новичков в Rust'е; некоторые из ее правил оказывались неудобными на практике. Настоящие изменения являются первым шагом, который мы предпринимаем на пути упрощения системы модулей.


В дополнении к вышеупомянутому изменению для макросов, есть два новых улучшения в использовании use. Во-первых, внешние пакеты теперь добавляются в prelude, то есть:


// было
let json = ::serde_json::from_str("...");

// стало
let json = serde_json::from_str("...");

Подвох в том, что старый стиль не всегда был нужен из-за особенностей работы системы модулей Rust:


extern crate serde_json;

fn main() {
    // это прекрасно работает; мы находимся в корне пакета, поэтому `serde_json`
    // здесь в области видимости
    let json = serde_json::from_str("...");
}

mod foo {
    fn bar() {
        // это не работает; мы внутри пространства имен `foo`, и `serde_json`
        // здесь не объявлен
        let json = serde_json::from_str("...");

    }

    // одно решение - это импортировать его внутрь модуля с помощью `use`
    use serde_json;

    fn baz() {
        // другое решение - это использовать `::serde_json`, когда указывается
        // абсолютный путь, вместо относительного
        let json = ::serde_json::from_str("...");
    }
}

Было неприятно получать сломанный код, просто перемещая функцию в подмодуль. Теперь же будет проверяться первая часть пути, и если она соответствует некоторому extern crate, то он будет использоваться независимо от положения вызова в иерархии модулей.


Наконец, use стал поддерживать импорт элементов в текущую область видимости с путями, которые начинаются на crate:


mod foo {
    pub fn bar() {
        // ...
    }
}

// было
use ::foo::bar;
// или
use foo::bar;

// стало
use crate::foo::bar;

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


mod foo {
    pub fn bar() {
        // ...
    }
}

mod baz {
    pub fn qux() {
        // было
        ::foo::bar();
        // не работает, в отличии от `use`:
        // foo::bar();

        // стало
        crate::foo::bar();
    }
}

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


Все эти изменения в совокупности упрощают понимание того, как разрешаются пути. В любом месте, где вы видите путь a::b::c, кроме оператора use, вы можете спросить:


  • Является ли a именем пакета? Тогда нужно искать b::c внутри него.
  • Является ли a ключевым словом crate? Тогда нужно искать b::c от корня текущего пакета.
  • В противном случае, нужно искать a::b::c от текущего положения в иерархии модулей.

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


Сырые идентификаторы


Вы можете теперь использовать ключевые слова как идентификаторы, используя следующий новый синтаксис:


// определение локальной переменной с именем `for`
let r#for = true;

// определение функции с именем `for`
fn r#for() {
    // ...
}

// вызов той функции 
r#for();

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


Приложения без стандартной библиотеки


Еще в Rust 1.6 мы объявили о стабилизации "no_std" и libcore для создания проектов без стандартной библиотеки. Однако, с одним уточнением: можно было создавать только библиотеки, но не приложения.


В Rust 1.30 можно использовать атрибут #[panic_handler] для самостоятельной реализации паники. Это означает, что теперь можно создавать приложения, а не только библиотеки, которые не используют стандартную библиотеку.


Другое


И последнее: в макросах теперь можно сопоставлять модификаторы области видимости, такие как pub, с помощью спецификатора vis. Дополнительно, "инструментальные атрибуты", такие как #[rustfmt::skip], теперь стабилизированы. Правда для использования с инструментами статического анализа, наподобие #[allow(clippy::something)], они еще не стабильны.


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


Стабилизация стандартной библиотеки


В этом выпуске были стабилизированы следующие API:


  • Ipv4Addr::{BROADCAST, LOCALHOST, UNSPECIFIED}
  • Ipv6Addr::{BROADCAST, LOCALHOST, UNSPECIFIED}
  • Iterator::find_map

Кроме того, стандартная библиотека уже давно имеет функции для удаления пробелов с одной стороны некоторого текста, такие как trim_left. Однако, для RTL-языков значение "справа" и "слева" тут приводят к путанице. Поэтому мы вводим новые имена для этих функций:


  • trim_left -> trim_start
  • trim_right -> trim_end
  • trim_left_matches -> trim_start_matches
  • trim_right_matches -> trim_end_matches

Мы планируем объявить устаревшими старые имена (но не удалить, конечно) в Rust 1.33.


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


Улучшения в Cargo


Самое большое улучшение Cargo в этом выпуске заключается в том, что теперь у нас есть индикатор выполнения!


demo gif


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


Разработчики 1.30.0


Множество людей совместно создавало Rust 1.30. Мы не смогли бы завершить работу без участия каждого из вас. Спасибо!


От переводчика: выражаю отдельную благодарность участникам сообщества Rustycrate и лично vitvakatu и Virtuos86 за помощь с переводом и вычиткой.

Комментарии (45)


  1. ozkriff
    29.10.2018 11:43

    Новый прогрессбар так себе помещается в узкие вертикальные консоли по 90-100 символов, но не могу найти опций cargo для его отключения. Никто не натыкался?


    1. freecoder_xx Автор
      29.10.2018 11:54
      +1

      Похоже, что не отключается. Придется терпеть )
      Хотя у меня нормально отображается и в узкой консоли:
      image


      1. ozkriff
        29.10.2018 14:08

        Да, походу в совсем узкой консоли имена компилируемых пакетов не показываются и все окей. А вот если сделать 90-100 шириной, то справа от индексов компилируемых пакетов показываются их имена и они уже не влезают в границу строки нормально. :(


  1. Laney1
    29.10.2018 11:56
    -2

    [route(GET, "/")]

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


    1. freecoder_xx Автор
      29.10.2018 12:13
      +2

      А в чем, собственно, проблема? Атрибуты в Rust реализованы не через строки, как в Go, они вполне себе парсятся и проверяются во время компиляции.


      1. Laney1
        29.10.2018 12:57
        -7

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


        1. Zanak
          29.10.2018 13:25
          +3

          На сколько я понимаю, аналогом данной конструкции в python являются декораторы. Если так, то вы безусловно не правы. При правильном применении, декораторы способны сделать ваш код проще, главное не перебарщивать с их использованием.


          1. Laney1
            29.10.2018 14:30
            -3

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


            Интересно, сколько из минусующих являются системными программистами, и сколько — обычными веб-макаками, не разбирающимися в теме?


            1. andreymal
              29.10.2018 14:35

              Давайте предположим, что здесь кто-нибудь не разбирается в теме — расскажите пожалуйста, чем это небезопасно?


            1. Zanak
              29.10.2018 14:56
              +1

              Когда кажется, что «весь мир сошел с ума, и только я нормальный» — это имеет свое название… :) (это к вопросу о макаках, не разбирающихся в программировании)
              Что же касается самого шаблона «декоратор», я прекрасно знаю о его плюсах и минусах, но готов выслушать ваше мнение. Или вы больше против экспериментов, как таковых, в языках программирования?


            1. humbug
              29.10.2018 15:39
              +1

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

              Ну так это же user(library)-defined правила, а не вшитые в язык. Не хотите — не пользуйтесь, генерируйте роуты ручками.


              Сравните подход go vs rust. Только угоротый подтвердит, что в go аннотирование структур через строки (которые проверяются только в рантайме) сделаны удачно.


              1. JekaMas
                29.10.2018 16:00

                Оооккк. Где записываться в "угоротые"?


                *Мне кажется, вы тоже несколько слишком категоричны.


            1. Hellpain
              29.10.2018 16:30
              +2

              Если вы системным программированием зовете скудность языка C, то это исключительно ваши проблемы восприятия мира системного программирования.


            1. freecoder_xx Автор
              29.10.2018 16:45
              +1

              Несмотря на то, что Rust позиционируется как системный язык программирования, он также является языком программирования общего назначения. Что плохого в том, что как системный ЯП Rust имеет развитую систему типов и довольно высокоуровневые абстракции с нулевой стоимостью, а как прикладной ЯП — высокую производительность?


              1. PsyHaSTe
                29.10.2018 22:21
                +6

                Тут скорее у человека связь «низкоуровенвый язык == обязательно страдать». У многих замечал, видимо, по историческим причинам.


                1. a-tk
                  30.10.2018 18:00

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


                1. n0dwis
                  31.10.2018 10:26

                  Почему? Мне ассемблер нравился в своё время. Давно, правда, с ним не сталкивался, но страданий не испытываю.


                  1. a-tk
                    31.10.2018 14:43
                    +1

                    Системное программирование != ассемблер.


                    1. n0dwis
                      01.11.2018 09:37

                      Да, но речь-то шла о

                      «низкоуровенвый язык == обязательно страдать»


      1. Sabubu
        29.10.2018 14:46

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


        1. freecoder_xx Автор
          29.10.2018 16:52

          Ну вот посмотрите, как это делается в Rocket:


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

          То есть атрибуту на откуп отдается указание метода и параметров запроса, а к корневой части пути разные обработчики монтируются в одном месте.


          1. Zanak
            29.10.2018 21:42

            Вы написали:

            #[get("/<name>/<age>")]
            а теперь представим, что в другом месте вы определили маршрут
            #[get("/<model>/<year_of_product>")]
            что при этом произойдет?


            1. mayorovp
              29.10.2018 22:12
              +3

              Если монтировать их на разные конечные точки — ничего особенного не произойдет…


            1. KIVan
              30.10.2018 02:36
              -1

              «представим, что в другом месте» — а если эти два маршрута определить в одном месте, проблема ведь никуда не денется, так? Её только заметят с большей вероятностью. Проблема тут возникает из-за использования сегментов пути для передачи параметров. Это в принципе плохо и проблемы с таким подходом будут всегда, вне зависимости от технической реализации.

              У URL есть разные специализированные части, en.wikipedia.org/wiki/URL#Syntax. Для передачи агрументов запроса служит «query», а в этом примере мы пытаемся передавать их через «path». «Path» предназначен для описания иерархичного пути (изначально соответствовал пути файла в ФС), к примеру, Application/Controller/Method. Такой путь практически гарантированно будет уникальным (1 путь = 0..1 файлов / методов). А в примере мы передаем в сегментах аргументы, для которых уникальность не является условием, отсюда и проблема с определением правильного маршрута.


            1. anonymous
              30.10.2018 06:26

              Как практик, отвечаю, в Rocket для этого есть аттрибут «rank».

              Ничего хорошего в нём нет, но приходится делать, то, что приходится :)


          1. Sabubu
            30.10.2018 14:04

            Не всегда получится сделать так, что у роутов будут разные префиксы. И даже если они разные, все равно, в реальном коде (не с функциями на 3 строчки как у вас), эти роуты будут далеко друг от друга раскиданы.


            1. ozkriff
              30.10.2018 14:12

              в других языках

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


            1. freecoder_xx Автор
              30.10.2018 14:18

              Ну и что с того? У вас же монтироваться они будут в одном месте — переходите к указанной функции-обработчику и все. Наоборот, удобно, что та часть запроса, которая должна соответствовать сигнатуре обработчика, указывается рядом с этой сигнатурой. Более того, так как она задается в процедурном макросе, мы можем проверять во время компиляции это соответствие.


            1. freecoder_xx Автор
              30.10.2018 22:55



        1. a-tk
          30.10.2018 18:01

          Вы про .htaccess сейчас?


          1. Sabubu
            31.10.2018 14:17

            Я про файл с конфигурацией роутинга, вроде routing.yml из Симфони.


            1. freecoder_xx Автор
              31.10.2018 14:54
              +1

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


    1. anonymous
      30.10.2018 06:23

      Частично соглашусь.

      Эти маршруты действительно красиво выглядят в документации (и в подходящих проектах, наверное), но у нас в итоге всё свелось к фактически одному маршруту на всё подряд (ну, к 4-ем, GET/POST/PUT/DELETE), а дальше мы запрос сами разбираем… (тут возникает справедливый вопрос, зачем нам Rocket, но просто пока времени нет всё на hyper переделать).

      Но всё же для разных задач — разные инструменты. Охотно допускаю, что где-то такие маршруты (и Rocket) могут быть удобны. Для более фиксированных API (наша проблема в том, что API очень уж динамичные).


  1. kvark
    30.10.2018 03:44

    Те ещё пляски с бубном вокруг модулей, да методов именований. Лучше бы о массивах фиксмрованной длинны подумали.


    1. AngReload
      30.10.2018 06:50

      Помню, не мог разобраться как макросы импортировать, мне нравятся сегодняшние изменения.
      А массивы, что с ними не так?


      1. naething
        30.10.2018 09:28

        А массивы, что с ними не так?
        Проблема 1: Массив фиксированной длины нельзя ициниализировать в цикле или, скажем, из среза или итератора. Например, если вы хотите заполнить массив длины N (где N — констана) числами от 0 до N-1, вам придется написать что-то вроде

        let a = [0i32; N];
        for i in 0..N {
            a[i] = i;
        }

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

        Проблема 2: Длину фиксированного массива нельзя сделать параметром шаблона. В C++ вы можете написать
        template<size_t N>
        struct Foo {
            int x[N];
        };
        В текущей версии Rust числа не могут быть параметрами шаблона, поэтому так сделать нельзя. Из этого вытекают многие другие проблемы. Например, нельзя реализовать какое-либо свойство (trait) для массивов произвольной длины. Если вы откроете страницу документации о массивах, то увидите, что реализации стандандартных свойств генерируются с помощью макросов для каждого N от 1 до 32: doc.rust-lang.org/std/primitive.array.html (в самом конце страницы). То есть, например, PartialEq реализовано для [u8; 32], но не для [u8; 33].


        1. freecoder_xx Автор
          30.10.2018 13:24
          +1

          В unsafe Rust первая проблема решается:


          let a = unsafe {
              let mut array: [i32; N] = std::mem::uninitialized();
              for i in 0..N {
                  std::ptr::write(&mut array[i], i);
              }
              array
          };


          1. red75prim
            30.10.2018 17:56
            +2

            Во многих случаях первая проблема и оптимизатором решается. godbolt.org/z/3HHGYt

            Но при длине массива 52 и больше оптимизатор почему-то сдаётся. Так что с unsafe всё-таки надёжнее.


    1. freecoder_xx Автор
      30.10.2018 13:11

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


  1. n0dwis
    30.10.2018 09:53

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


    1. freecoder_xx Автор
      30.10.2018 11:31
      +1

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


      • Использовать Rc, RefCell и прочие Weak; или
      • Использовать сырые указатели и управлять освобождением памяти вручную; или
      • Хранить узлы во внешнем контейнере и ссылаться на них по индексу; или
      • Использовать готовые библиотеки, которые скрывают всю кухню за своим API.

      Вообще, для лучшего понимания, рекомендую изучить реализацию rust-forest. Там представлено несколько подходов и они довольно неплохо прокомментированы.


      1. n0dwis
        30.10.2018 12:02

        Не совсем тривиальна. Нужно ведь хранить стек предков + дерево узлов. И в той и в той структуре должна быть ссылка на один и тот же узел. И поскольку всё это в цикле — компилятор не может определить время жизни и ругается (насколько я понял).
        Можно, правда, добавлять узел в дерево только после обхода всех его потомков, это снимает проблему, но, боюсь, такое подойдёт не во всех случаях.

        Вообще, для лучшего понимания, рекомендую изучить реализацию rust-forest. Там представлено несколько подходов и они довольно неплохо прокомментированы.

        Спасибо большое! Изучу.


    1. freecoder_xx Автор
      30.10.2018 12:17
      +2

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


      Допустим у вас есть некий объект и вы хотите хранить ссылки на него в нескольких местах одновременно. Но тут возникает сразу две проблемы:


      1. Объект должен быть доступен из разных мест по ссылке, и удален только тогда, когда на него нет ссылок. Если есть по крайней мере одна ссылка, то объект должен жить.
      2. Правила Rust позволяют иметь больше одной ссылки только на неизменяемый объект. Ссылка на изменяемый объект может быть только одна, без каких либо других ссылок на этот объект.

      Первая проблема решается подсчетом ссылок (reference counting) и в других популярных прикладных ЯП она обычно скрыта от пользователя за реализацией. То есть любая "ссылка" на объект в таких ЯП всегда есть умный указатель со счетчиком ссылок. В Rust же такого типа ссылку нужно создавать вручную, если она вам нужна. Для этого используются типы Rc и Arc. Кроме того, нужно как-то решать вопрос образования циклических ссылок (Weak).


      Вторая проблема решается введением совместно используемых изменяемых контейнеров — это типы Cell, RefCell, Mutex, RwLock.


      Я рекомендую прочитать вот эту страницу официальной документации и разобрать приведенный там пример с гаджетами для того, чтобы понять, для чего и как использовать Rc, RefCell и Weak вместе. Также рекомендую ознакомиться с документацией по cell.


      1. n0dwis
        30.10.2018 12:33

        Большое спасибо за разъяснения!


  1. freecoder_xx Автор
    30.10.2018 22:59
    +1

    Обещанный блог-пост таки вышел, слегка с опозданием: Help test Rust 2018