Rust и Я

У меня, как наверное и у многих других C++ разработчиков какие-то сложные отношения с Rust. Этот язык вроде как и очень сильно нравится и в то же время всё сложно. На сей раз моя попытка заняться Rust в серьез зашла куда дальше чем обычно (обычно я после нескольких небольших тестиков удовлетворенно забрасывал изучение) – я нашел компанию которой нужны Rust разработчики, связался с ними и взял тестовое задание. Тестовое задание просто восхитительное, не сложное, но помогает понять главное – хочешь ли ты связываться с языком или нет?

На тестовое задание я честно потратил часов 8 и за это время успел заглянуть в tokio, которая, как я понимаю, является образцово-показательной библиотекой для написания асинхронных сетевых приложений. Честно говоря, я до сих пор в состоянии смятении, но точно знаю одно – видеть такое и не дай бог поддерживать на постоянной основе я точно не хочу. К примеру, вот кусок кода, который читает данные из канала и шлет их по TCP:

Box::new(tcp.map(move |stream| {
    let (sink, stream) = stream.framed(Bytes).split();
    pool.execute(stdin.forward(sink).then(|result| {
        if let Err(e) = result {
            panic!("failed to write to socket: {}", e)
        }
        Ok(())
    })).unwrap();
    stream
}).flatten_stream())

Несколькими месяцами раньше я баловался с биндингами к libClang, где было всё читабельно и очевидно, что наводит на мысли, что написать на Rust можно писать как в адекватном и легко поддерживаемом стиле, так и городить write-only код в духе приведенного выше. И надо отметить что в примере выше чувствуется и стиль и задумка. Если в него какое-то время повтыкать (желательно в IDE с навигацией, очень рекомендую IntelliJ Rust), то становится понятно что хотел сказать автор и почему. Только вот “НУ ЗАЧЕМ?!?!” не покидает.

В итоге мне в очередной раз стало казаться, что за пределами небольших сообществ гиков Rust взлететь не может, так как поддерживать столь неочевидные решения должно быть даже дороже чем решения на C++ с ручным управлением памятью (в конце концов, санитайзерами можно отловить практически всё). Не знаю чем это вызвано, скорей всего тем, что Rust развивает комьюнити гиков, а C++ комитет старперов. В результате в Rust тянут все, что круто выглядит, а в C++ только то, что “мегамозги” одобрили. Побочным эффектом этого выступает то, что в Rust многие удобные вещи доступны сразу или почти сразу, а в C++ это “наверное будет реализовано после C++20” (да-да, это я про Meta).

Наверное, меня всё равно не отпустит и баловаться с Rust я не перестану. Но вот подаваться на позицию “Rust разработчик” я довольно долгое время точно не буду, сообщество должно наиграться в “zero cost abstractions” для начала

IDE и C++

Я довольно давний поклонник IDE от Jetbrains. Тем же IntelliJ IDEA и PyCharm просто нет сопоставимых альтернатив с точки зрения скорости работы и удобства. Собственно говоря у меня у меня даже подписка на них оформлена из соображений “нравится проект – поддержи финансово”, но вот с CLion что-то пошло не так…

На мой взгляд основная проблема в том, что разработчики и менеджеры продукта пришедшие с платформ .NET и JVM просто не понимают что такое разработка на C/C++. В итоге идёт попытка переложить модель разработки с той же Java на C++. В крайне случае у меня сложилось именно такое ощущение как после личного общения с JetBrain-овцами на CppCon, так и после вчерашнего диалога в Twitter.

Собственно, проблема и непонимание состоит в том, что команда CLion упорно хочет видеть “проектную модель”. Я не знаю чем это вызвано, толи наличием libClang под капотом, толи архитектурными проблемами собственного парсера, но без реальной проектной модели CLion превращается в тыкву. Хотя, он даже и с реальной проектной моделью иногда превращается в тыкву, например спотыкаясь на сгенерированном через Protobuf коде.

К сожалению, такой подход не может работать в мире C++ на многих проектах сложнее Hello World. За исключением счастливчиков, которым доступно писать всё в Visual Studio, у разработчиков огромный зоопарк используемых редакторов и причина у этого довольно грустная, в теории CLion и должен был исправить эту ситуацию. Так вот, мои коллеги пишут в Sublime, Emacs, Vim, Eclipce CDT, Studio Code и многих других редакторах по большому счету потому, что все они практически одинаково плохи в роли C++ IDE. Ну может за исключением Emacs/Vim, за счёт его интеграции с Globals/CTags и Eclipse, за счёт наличия грамотно реализованного парсера C++. Но в Emacs/Vim ещё нужно уметь писать – это свой отдельный мир, а Eclipse даже на Xeon-ах с горой памяти на борту умудряется тормозить.

Если же говорить о проектной модели и почему привязка к ней делает редактор не пригодным к промышленному использованию – то её часто просто нет. К примеру на данный момент я периодически переключаюсь между несколькими проектами: один на базе CMake, второй на некой дикой смеси Make, CMake и SCons обвешанной sh/bat скриптами сверху. Так же я иногда заглядываю в проекты которые представляют из себя кучу файлов просто “для консультации”. Таким образом, в теории, я мог бы писать 1 проект из всех над которыми работаю в CLion, но есть ли в этом хоть какой-то смысл если мне нужно писать и другие проекты и иметь пусть приблизительную, но быструю навигацию по коду и хоть какую-то автоподстановку? По мне так никакого, так как привыкать к особенностям 2-х редакторов куда как менее удобно нежели всегда работать в одном. Жаль только надежда на то, что на менеджеров продукта CLion сойдет понимание проблемы угасла.

Go и контроль качества проекта

А начну я с громкого заявления: Go – это один из самых лучших языков программирования для командной разработки на данный момент. И дело тут ни в простоте языка, ни в простоте инфраструктуры, хотя и эти пункты очень важны. Основная же причина для такого заявления кроется в инструментах для статического анализа кода. При этом я не знаю ни одного другого языка, который позволял бы провести такой дотошный статический анализ по всем направлениям, начиная от стилистики кода, заканчивая потенциально опасными конструкциями и обработкой ошибок.

Основная проблема в командной разработке – разнородность уровня и, как следствие, разное качество производимого командой кода. Ревью, безусловно, позволяют в той или иной степени сгладить последствия разнородности и даже подтянуть уровень разработчиков, но работают они не так хорошо как хотелось бы. Кому-то может быть лень, кто-то устал, кто-то не заметил и в репозитории оказывается нечто, которому там не место. Можно ли это хоть как-то исправить в C++? Нет, нельзя. Можно ли эту проблему минимизировать в Go? Довольно легко, что я вчера и сделал.

В Git есть замечательный механизм хуков, которые отрабатывают в зависимости от внутренних событий. Меня интересовал pre-commit хук, позволяющий заблокировать коммит по результату выполнения скрипта. Использование этого хука купе с Go Meta Linter, агрегатором линтеров для Go, позволяет автоматически заблокировать коммит до исправления ошибок.

#!/bin/sh

if ! [ -x "$(command -v gometalinter)" ]; then
echo 'Error: gometalinter is not installed. Please install it first and execute `gometalinter -i`'
exit 1
fi

lint_errors=$(gometalinter ./... --vendor -j 5)
if [[ $? != 0 ]]; then
echo 'Error: gometalinter checks had failed. Please execute `gometalinter ./... --vendor` first and fix ALL issues'
exit 1
fi

Дело остается за малым – поместить файл с хуком в директорию .git/hooks проекта, что я сделал при помощи нашего сборочного скрипта. Да, нам пришлось написать такой скрипт поверх стандартных Go команд типа go build так как есть пре- и пост- шаги которые необходимо предпринять в процессе сборки, а никакого CMake-подобного механизма в Go нет. Хотя, что уж тут, поверх CMake писать аналогичный скрипт тоже приходится.

Эксперименты с Rust

В течении последних лет я время от времени с интересом поглядывал на Rust, наверное, с версии 0.3. За это время язык претерпел много изменений, за счет чего он то нравился, то совсем не нравился, то вызывал сомнения. Но не смотреть на него было никак нельзя, так как языков, подходящих для “коробочной” разработки и не генерирующих байт-код, по пальцам одной руки пересчитать можно и еще свободные пальцы останутся.

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

Подготовка к работе

Rust, конечно, не может похвастаться таким изобилием редакторов, как C++ или Go, но для относительно квалифицированного разработчика, привыкшего к командной строке, того, что есть, более чем достаточно. При этом управление зависимостями, создание проектов, установка внешних утилит выше всяких похвал.

Установка

Сам компилятор и набор утилит типа cargo устанавливается и обновляется очень легко и удобно, особенно на UNIX-ах. Всего одна консольная команда в соответствии с официальной документацией. Windows никогда особо дружелюбным для разработчиков не был, хотя и тут разработчики Rust постарались и сделали как поддержку GNU toolchains, так и поддержку MSVC. Всё заработало “из коробки”, что несказанно меня удивило. Ощущение зрелости как минимум системы доставки и развертывания однозначно присутствует.

Редактор

В моём случае с редактором было всё просто: взять Vim да установить пару-тройку дополнений к нему. Подсветка и форматирование работают отлично, автодополнение, базирующееся на racer, работает сносно. Как я понял, все остальные редакторы и IDE также используют racer для автодополнения и ожидать какого-то чуда за пределами VIM не следует. Понадобилось всего два плагина: rust.vim и vim-racer. В обоих случаях они снабжены достаточно подробной документацией и никакой сложности установка и настройка не вызывает.

Особо хотелось бы отметить наличие rustfmt, который позволяет форматировать код в едином стиле. Впервые я проникся любовью к такого рода приложениям, начав работать с Go, где весь код автоматически форматируется одним единственным правильным способом. В командной работе практика оказалась невероятно удобной, так как из “стандарта кодирования” команды полностью исчезает пункт про форматирование, при этом весь код выглядит одинаково, что облегчает понимание и работу с чужими фрагментами/модулями. После этого я внедрил у нас использование clangformat на C++ проектах (правда уже с единым набором правил для форматирования), что также сделало разработку ощутимо комфортнее.

Также в Vim большой популярностью пользуется плагин Tagbar, который не умеет работать с Rust из коробки, но это очень легко исправляется.

Еще одним потенциально полезным компонентом экосистемы Rust мне показался cargo-outdated, позволяющий отслеживать устаревшие зависимости. Ну просто мечта разработчика C++ какая-то, особенно учитывая то, что сторонние компоненты (дополнения) Rust устанавливаются при помощи cargo очень простым способом:

cargo install rustfmt
cargo install racer
cargo install cargo-outdated

Создание проекта

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

cargo new --bin ddump-gen

И у меня есть новый проект, в котором, путем добавления всего двух строчек, я могу работать с libclang:

[dependencies]
clang = "0.16.0"

Это интересно само по себе хотя бы тем, что изначально я порывался написать тот же проект на C++, но бросил уже на стадии написания cmake-файла для этой же самой libclang.

Разработка

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

Доступные библиотеки

То, что Rust активно пиарится Mozilla и Samsung в течении последних лет, очень положительно сказалось на количестве доступных новых библиотек, а также оберток для уже существующих библиотек из мира C/C++. В результате очень быстро подобралась подходящая обертка над libClang, был найден удобный парсер командной строки и подходящий логгер. Довольно удивительно, но на решение тех же вопросов на C++ я бы потратил даже больше времени, чем в совершенно незнакомой экосистеме.

Про язык

С самим языком всё не столь однозначно и прекрасно. Наверное, если бы не опыт последних месяцев с Go, я бы просто подивился тому, почему же так неудобно обрабатывать ошибки, и отложил бы эксперименты до “лучших времен”. Но вот после того, как желание обязательно иметь в языке исключение прошло, оказалось, что с кодами возврата, особенно при наличии сопоставления с образцом, жить можно довольно комфортно.

Собственно говоря, к чему весь этот разговор про обработку ошибок. При отсутствии исключений в языке размер кода растет, так как, во-первых, появляется необходимость проверять коды возврата и не ожидать, что “исполнилось корректно либо выполнение было прервано”, а во-вторых, теряется возможность сделать обобщенную обработку нескольких потенциальных ошибок в одном блоке. Если задуматься над причиной, то её можно сформулировать как “мне нужно писать больше кода, но лень”. Есть ли в такой необходимости что-то хорошее? Опыт с Go подсказывает, что есть! Адресная обработка ошибок, т.е. сразу после возникновения ошибочной ситуации, обычно более качественно продумана и лучше покрыта диагностикой по сравнению с обобщенными исключениями. Кроме того, в стандартной библиотеке Rust пошли чуть дальше и добавили немного синтаксического сахара. Особо полезными, на мой взгляд, являются функции семейства unwrap(). Например, я точно знаю, что библиотека разбора аргументов командной строки не пропустит пустого значения для параметра SRC_FILE, и могу смело писать:

let path = matches.value_of("SRC_FILE").unwrap();

Смело потому, что попытка unwrap() для None обернется паникой и аварийным завершением приложения. Но для работы с такими ситуациями есть не менее удобный unwrap_or()

let std = matches.value_of("STD_VER").unwrap_or("11");

Среди относительно новых изменений языка числится `?`-синтаксис, которым я так и не решился воспользоваться, так как, на мой взгляд, он делает код просто невероятно сложно поддерживаемым и является злом по определению. Лучше уж отдать предпочтение макросу try!(), который делает то же самое, но более очевидным путем, а еще лучше либо явно звать unwrap(), либо писать полную обработку на базе match.

Ужас прошлых версий с временем жизни объектов разработчики спрятали, и теперь код можно, обычно, писать, особо не задумываясь о том, как удовлетворить компилятор. В версиях около 1.0 это был невероятно сильный отталкивающий фактор, так как вместо решения проблемы ты занимался удовлетворением хотелок компилятора буквально с первой линии кода. Само собой, правила никуда не делись, но компилятор стал ощутимо умнее.

В то же время мне сильно не нравится количество смысловой нагрузки на одну линию кода. Даже в простейшем примере выше с unwrap_or() видно, что тут упаковано и создание переменной с присвоением ей результат вызова функции, и условная проверка с возвратом значения по умолчанию, если функция ничего не вернула. Многовато для одной строки далеко не самого сложного кода на Rust.

Кому может быть интересен

В интернете можно найти довольно много мнений и споров на тему того, является ли Rust “убийцей C++” или нет. Смешные дискуссии, особенно с учетом того, сколько своих “убийц” C++ пережил. Поэтому я не буду говорить о том, кого же Rust вытеснит, только время рассудит, но вот кому и когда он может пригодиться, уже более интересно.

Во-первых, на современном Rust писать удобно и комфортно, инфраструктура хороша и проблем не вызывает. Во-вторых, появилось много библиотек в разной степени готовности (зачастую полностью готовых к коммерческому использованию), которые можно использовать и не заниматься решением утилитарных задач. В-третьих, детские болезни компилятора и языковой стилистики в основном исправлены. В-четвертых, сообщество Rust очень-очень активное и доступно большое количество вводных сайтов и полезной документации.

По моим ощущениям, при помощи современного Rust проблемы могут быть решены как минимум не хуже, чем при помощи современного C++, и более качественно, чем на современном Go. Так, для C++ программиста, активно использующего C++11 с STL и BOOST в повседневной работе, Rust может показаться ограниченным языком, особенно за счет куда как более бедной системы метапрограммирования и особенностей обработки ошибок. В то же время, для разработчика, активно использующего C++ с Qt или тот же Go, всё должно быть наоборот, приятно и комфортно. Если говорить про порог входа, то, с одной стороны, он однозначно выше, чем в случае с Go, начать писать на котором можно через 3-4 дня активного изучения документации и лучших практик. С другой стороны, информация, с которой придется ознакомиться, куда менее объемная и запутанная, чем даже минимальное вводное чтиво по C++.

Для себя я сделал вывод, что буду и дальше использовать Rust в “домашних проектах” и, при возможности, протолкну его продуктовую разработку в команде. Жизнь будет проще, жизнь будет веселее!

Vim и проверка орфографии

Так как мой основной рабочий инструмент – Vim (в комбинации с ZSH) я давно хотел прикрутить к нему проверку орфографии. Да и что там, собственно, прикручивать – все идет “из коробки” и остается только подключить то, что нужно. Оказалось всё и вправду проще не придумать.

Во-первых, нужно слегка подправить .vimrc для автоматического включения проверки орфографии на определенных типах файлов/буфферов:

autocmd BufRead,BufNewFile *.md setlocal spell
autocmd BufRead,BufNewFile *.txt setlocal spell
autocmd FileType gitcommit setlocal spell

Ну и потом не забыть о паре комбинаций:
z= – отобразить список замен;
zg – добавить слово в список корректных слов.

И работать как-то приятнее стало

Форматирование C++ кода

Одна из особенностей языка Go, которая мне очень нравится – стандартизация практические всего и вся с предоставлением инструментов для валидации и максимальной автоматизации применения. Так все программы на Go выглядят более-менее одинаково как за счет единого стандарта к разработке (да,я не люблю кучу соплей с проверкой результатов возврата, но тем не менее это единообразие) так и за счет единого форматирования. Благодаря этому не приходится испытывать какого-то серьезного дискомфорта разбирая новый кусок кода – каким бы (не)качественным он ни был, выглядеть и как следствие восприниматься он будет как родной. Кроме того, основная масса редакторов Go поддерживает переформатирование текста при сохранении, так как за формат отвечает косольное приложение, то появляется возможность поставить триггеры в VCS и отклонять не удовлетворяющие условиям коммиты. С одной стороны, все это может казаться мелочами. Но только до тех пор, пока ты не работаешь в довольно сильно распределенной команде с крайне разными уровнями у разработчиков.
Continue reading

Мелкие пакости: время жизни переменной в Rust

Допустим, хочется получить текстовое представление типа переменной в Rust. При этом в язык входит такая замечательная функция как type_name() -> &’static str принимающая тип выдающая его тектовое обозначение. Само собой, хочет применить его не только для типа (название типа не так уж и полезно в диагностических целях), а к переменной. Логичным для C++ разработчика выглядит приблизительно следующее решение:

fn type_of<'a, T>(_: T) -> &'a str {
    unsafe { std::intrinsics::type_name::<T>() }
}

Но тут возникнет довольно занятная проблема, так как переменная становится недоступной после (с некоторыми ньюансами в зависимости от типа) получения её имени:

error: use of moved value: `*variable_name` [E0382]

После небольшой фрустрации понимаешь, что в принципе это ж фича и компилятор не должен догадываться о моих намерениях только лишь получить тип, а не реально использовать значение. Но делать что-то нужно. Единственным подходящим решением оказывается передача по ссылке (ссылке в понимании Rust, а не C++), что ожидаемо, но немного странно для C++ разработчика.

fn type_of<'a, T>(_: &T) -> &'a str {
    unsafe { std::intrinsics::type_name::<T>() }
}

Вообще, все эти мелкие пакости модели памяти постоянно преследуют при программировании на Rust. Никак не могу понять, это реально зло или я просто еще не привык и просто мыслю моделью памяти C++?

Генератор CMakeLists.txt файлов

Довольно часто возникает необходимость быстренько написать тестовое приложение на C++ и опробовать в нем что-то. IDE я не слишком люблю, а каждый раз где-то выискивать завалявшийся шаблон к CMake-у довольно лениво. После очередных поисков запилил небольшой вспомогательный скриптик (само собой на Python) для генерации CMakeLists.txt.

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

Сам скриптик с руководством по использованию тут: https://github.com/astavonin/gen-cmake

C++XX в CMake

Вообще я очень люблю использовать CMake для создания различных небольших тестов. Собирается везде, ручной работы ощутимо меньше, чем если писать правила для Make, генерируется поддержка для любой IDE (если тестик в что-то более крупное перерастет и т.д). И как-то меня угораздило “проспать” как CMake 3.x так и довольно полезную фифу в нем – простое и понятное подключение поддержки C++11. Я всегда подключал C++11 по старинке:

list( APPEND CMAKE_CXX_FLAGS "-std=c++11")

Но, оказывается-то, прогресс шагнул далеко вперед! так что для тех кого так же как и меня “заморозили” сообщаю – все стало проще и понятнее:

cmake_minimum_required(VERSION 3.1 FATAL_ERROR) # (1)
project(cpp11_test)

add_executable(cpp11_test main.cpp)

set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD 11)          # (2)
set_property(TARGET ${PROJECT_NAME} PROPERTY CXX_STANDARD_REQUIRED ON) # (3)

Прекрасная фича доступна начиная с CMake 3.1 1 и включается она 2 очень просто. Если какие-то обходные пути при отсутствии у компилятора поддержки C++11 не планируются, то стоит объявить 3 наличие поддержки C++11 обязательной.

Релиз Rust 1.0. Возможности и сомнения

Разработчики Rust прошли долгий путь и 15 мая ожидается выпуск первой версии языка с вечеринкой по случаю новорожденного. Лично я долго ждал этого события, хотя и вызывает оно довольно смешанные чувства. Вроде что-то и родилось, но хочу ли я это что-то использовать и стоит ли оно того? Вот в чем вопрос. Дело в том, что в процессе развития Rust претерпел довольно сильные изменения и теперь это далеко не тот же язык, о котором я писал в 2013.
Continue reading