Советы по написанию библиотек на Rust

оригинал: Pascal Hertleif • перевод: Андрей Лесников • руководства • поддержите на Patreon

Прошёл примерно год, как меня заинтересовал Rust, язык программирования от Mozilla Research, сосредоточенный на решении трёх задач: безопасность, скорость и параллелизм. Он такой же низкоуровневый, как Си или C++, имеет хорошую систему типов (с обобщениями (generics) и типажами (traits)), дружелюбный компилятор и отличный менеджер пакетов Cargo.

С выпуска Rust 1.0 прошло уже пол года (май 2015): многие библиотеки (пакеты, crates), включая некоторые мои, были опубликованы в центральном регистре crates.io. Вот неплохие практики (ещё рановато называть их «лучшими»), которые помогут другим людям находить, использовать и дополнять вашу библиотеку.

Содержите свой код в чистоте

Самое главное, конечно, это сам ваш код. Rust по умолчанию проверяет множество вещей, но есть кое-что ещё, что можно сделать для улучшения кода.

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

rustfmt

rustfmt автоматически переформатирует ваш код, повышая его понятность людям, особенно не_авторам (машины не в счёт — они поймут любой код, раз он компилируется) (прим. переводчика: и ещё пресечёт бесполезные споры о стилях). И он неплохо справляется с этим, ну, по крайней мере, ничего не напортил в моем коде. Просто время от времени запускайте эту команду (прим. переводчика: или добавьте в вашем редакторе автоматический прогон rustfmt при сохранении файлов с расширением .rs), чтобы ваш код стал выглядеть аналогично всему остальному коду на Rust:

1
$ rustfmt src/lib.rs

Несмотря на то, что ведутся попытки определить Единый Истинный Стиль Rust (The One True Rust Style), и rustfmt старается по умолчанию следовать им, вы можете найти список опций форматирования тут. На данный момент, большая часть моих проектов использует следующие настройки (сохраните это в файл rustfmt.toml в корне своего проекта):

1
2
format_strings = false
reorder_imports = true

(прим. переводчика: Кстати, теперь при установке через cargo install rustfmt становится доступна подкоманда cargo fmt. Но лично меня бесит форматирование rusfmt, и до конца исправить его не помогли никакие опции, так что для себя решил повременить с его внедрением.)

Используйте больше проверок

«Проверки» (lints) — это небольшие плагины компилятора, которые проверяют ваш код во время сборки (в основном на стилистические ошибки). По умолчанию rustc уже включает кое-какие предупреждения, например dead-code (предупреждает о мёртвом коде) или non-snake-case (предупреждает, если имена некоторых элементов не в змеином_регистре (snake_case)).

Есть ещё кое-какие очень полезные встроенные проверки. Вы так же можете превратить их в полноценные ошибки сборки, заменив #![warn(...)] на #![deny(...)] (смотрите в справочнике). Я обычно добавляю в свои проекты вот эти:

1
2
3
4
5
6
7
8
9
#![deny(missing_docs,
        missing_debug_implementations,
        missing_copy_implementations,
        trivial_casts,
        trivial_numeric_casts,
        unsafe_code,
        unstable_features,
        unused_import_braces,
        unused_qualifications)]

Особенно полезен самый первый — missing_docs: он срабатывает, если в коде есть любые недокументированные публичные интерфейсы. unsafe_code тоже может быть очень полезен, благодаря ему ваша библиотека выглядит более надёжной в глазах людей.

Список всех доступных проверок вместе с их описанием можно посмотреть в выводе команды rustc -W help.

Больше проверок Богу Проверок!

Уверен, вы уже успели заметить, что проверки (lints) — это хорошо. Поэтому авторы clippy написали ещё сотню (на самом деле, около 68 для версии 0.0.21 (прим. переводчика: уже больше)). Поскольку clippy является плагином компилятора, то на данный момент для работы с них вам понадобиться ночная сборка rutsc, которую можно вызывать разными способами: например, используя cargo-clippy, или добавляя его в виде необязательной зависимости своего проекта.

Мне больше нравится последний вариант, поэтому добавил вот это в свой Cargo.toml:

1
2
3
4
5
6
[dependencies]
clippy = {version = "0.0.21", optional = true}

[features]
default = []
dev = ["clippy"]

Эти настройки делают clippy необязательной зависимостью и включают его, только если установлен флаг dev. В главном файле пакета вы можете дополнительно включить его вот так:

1
2
3
#![cfg_attr(feature = "dev", allow(unstable_features))]
#![cfg_attr(feature = "dev", feature(plugin))]
#![cfg_attr(feature = "dev", plugin(clippy))]

Сборка вашего проекта при помощи cargo build --features "dev" теперь автоматически запускает проверки clippy. (Само собой, вам нужно разрешить unstable_features, только если вы до этого запрещали их).

Предупреждение: плагины компилятора в данный момент нестабильны. Если вы обновите новую ночную сборку компилятора, clippy может сломаться. Хотя авторы clippy обычно все быстро чинят.

(прим. переводчика: относительно недавно появилась онлайн версия clippy, теперь можно не возиться с локальной установкой ночного rustc, и есть повод поставить ещё одну лычку (badge) в Readme.md!)

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

Тесты

В Rust встроена изумительная поддержка тестов: вы можете быстро написать тесты для своих модулей прямо внутри них же, а cargo автоматически запустит их (*.rs файлы в директории tests/). О, и примеры в документации (или в examples/) тоже будут протестированы.

Больше тут и сказать-то особо нечего. Просто прочитайте главу в официальной книге (перевод).

Инфраструктура проекта

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

Метаданные Cargo

Первое (и самое простое), что стоит сделать для облегчения поиска вашей Полезной Библиотеки, — это заполнить файл Cargo.toml. Там есть множество полей с метаданными для crates.io. Вот пример из моего пакета HSL:

1
2
3
4
5
6
7
8
9
10
[package]
name = "hsl"
version = "0.1.0"
authors = ["Pascal Hertleif <my@email.address>"]
repository = "https://github.com/killercup/hsl-rs.git"
homepage = "https://github.com/killercup/hsl-rs.git"
license = "MIT"
readme = "README.md"
documentation = "http://killercup.github.io/hsl-rs/"
description = "Represent colors in HSL and convert between HSL and RGB."

Кстати говоря, не используйте '*' версии для зависимостей — crates.io отклонит такие пакеты, поскольку они игнорируют семантическое версионирование, на которое опирается Cargo. Также, для упрощения добавления последней версии пакета в зависимости, можно использовать cargo-edit.

README.md

Люди, смотрящие на стартовую страницу вашего хранилища, скорее всего увидят содержимое файла Readme.md. Убедитесь, что в нем есть ответы на стандартные вопросы:

(прим. переводчика: Кстати, насчёт лицензии: в январе сообщество Rust перевело большую часть значимых проектов на двойственную MIT/Apache-2.0 лицензию и советует использовать её в дальнейшем по умолчанию. Аргументация есть, например, в задаче библиотеки cgmath.)

Readme.md также является отличным местом для размещения небольших примеров использования вашей библиотеки: часто людям удобно начинать разбираться с библиотекой именно с них. Чтобы удостовериться в работоспособности примеров в Readme.md (и в другой документации в формате Markdown), можно использовать skeptic. Путём добавления небольшого хука (hook) в сборочный процесс Cargo (в файл build.rs), вы можете вызывать skeptic для превращения примеров кода в Markdown файлах в обычные тесты. (Подробности смотрите в документации skeptic.)

Другие метафайлы

Не забудьте добавить файл .gitignore, в котором стоит запретить git’у отслеживание директории target/ (прим. переводчика: Cargo использует эту директорию для всяких временных файлов и артефактов сборки, так что нет никакого смысла фиксировать её в СКВ). Для библиотечных пакетов (т.е. не дающих на выходе исполняемый файл) вы должны игнорировать и Cargo.lock файл (прим. переводчика: почему).

Другой файл, который я стараюсь добавлять в каждый проект, — это .editorconfig. Я использую следующие настройки:

1
2
3
4
5
6
7
8
9
10
11
12
root = true

[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4

[*.md]
trim_trailing_whitespace = false

Непрерывная интеграция

Если ваш открытый проект размещается на GitHub’е, вы можете бесплатно воспользоваться сервисом непрерывной интеграции Travis CI. Чертовски полезная штука, так как позволяет настроить автоматический запуск тестов в различных окружениях (например, для стабильной, бета и ночной версии Rust или разных архитектурах) для каждого отправленного изменения (commit) или запроса на включение.

Используя travis-cargo вы можете запускать в Travis тесты и бенчмарки, генерировать документацию (и отправлять (push) её на GitHub Pages), считать тестовое покрытие (и отправлять его в Coveralls).

Я использую примерно такой шаблон .travis.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
sudo: false
language: rust
rust:
- nightly
- beta
- stable
matrix:
  allow_failures:
  - rust: nightly
before_script:
- |
  pip install 'travis-cargo<0.2' --user &&
  export PATH=$HOME/.local/bin:$PATH
script:
- |
  travis-cargo build &&
  travis-cargo test &&
  travis-cargo bench &&
  travis-cargo --only stable doc
addons:
  apt:
    packages:
    - libcurl4-openssl-dev
    - libelf-dev
    - libdw-dev
after_success:
- travis-cargo --only stable doc-upload
- travis-cargo coveralls --no-sudo
notifications:
  email:
    on_success: never
env:
  global:
  - TRAVIS_CARGO_NIGHTLY_FEATURE=dev
  - secure: # тут всякое зашифрованное

(прим. переводчика: в оригинале это называется «базовой конфигурацией», но я тут не согласен, ведь «базовая» — это просто language: rust)

Эта конфигурация имеет несколько опций специально для запуска clippy: нам не критичен результат сборки ночным компилятором (интерфейс плагинов может измениться, и все сломается), и ночной компилятор вызывается с флагом dev.

Travis умеет собирать под Linux и Mac OS X. Для тестирования под Windows стоит взглянуть на AppVeyor (он тоже бесплатен для открытых проектов). (прим. переводчика: Очень полезный, но страаашно мееедленный, по сравнению с Тревисом, сервис.)

Автогенерация документации

Чтобы включить автоматическую загрузку сгенерированной документации (т.е. отправлять (push) вывод rustdoc в ветку gh-pages), поддерживаемую в travis-cargo, вы должны добавить переменную окружения GH_TOKEN, которая содержит маркер доступа (access token) к вашей учётной записи на GitHub (с ограниченными правами). Вы можете создать её вот тут (У меня есть по одной для каждого проекта). Чтобы зашифровать маркер, вы можете использовать утилиту TravisCLI (устанавливаемую при помощи gem install travis) путём запуска её в корневой директории проекта (замените 1234 на свой маркер):

1
$ travis encrypt "GH_TOKEN=1234" --add env.global

Когда все настроено корректно, вы должны увидеть документацию проекта на имяпользователя.github.io/имяпроекта, например killercup.github.io/hsl-rs.

(Прим. переводчика: Запихивание отрендеренной в html документации в ветку проекта, хоть и является очень распространённой практикой, но мне кажется так себе идеей. В идеале, crates.io должен сам дёргать rustdoc и хранить/показывать его вывод (в духе godoc.org). Пока есть неплохой костыль в виде crates.fyi.)

Homu

Используя непрерывную интеграцию, вы можете быть уверены, что код в запросе на включение (pull request) работает и готов к слитию (merge). А с последними изменениями в гитхабе — защищёнными ветками с обязательными проверками (status checks) (перевод), вы можете быть уверены что все тесты прошли перед слиянием. Но вы не можете быть уверены, что тесты пройдут после вливания ветки, а мастер!

Проект Rust на GitHub использует интеграционного робота Bors, чтобы решить эту проблему: вместо слияния запросов на включение самим, они говорят Борсу заняться этим. Борс берет по одному запросу на включение (у него есть очередь), сливает его в текущий master, прогонят все тесты и, если они проходят успешно, отправляет новую версию в ветку master. Это значит, что влитие большого количества запросов на включение может занять больше времени, но зато можно быть спокойным, что в мастере всегда выполняются все тесты.

Ядро Борса называется homu и может использоваться и в ваших проектах. Просто добавьте пользователя homu в соавторы на гитхабе, зарегистрируйте свой проект на homu.io и сливайте запросы на включение при помощи комментария @homu r+!

Ещё трюки

Хотите больше? Хорошо, вот ещё несколько советов:

(прим. переводчика: Я не особо согласен насчёт использования Clog. Вот, как по мне, отличный журнал изменений, такое автоматически не получится сделать. Но, наверное, лучше автоматический журнал изменений, чем вообще никакого.)

Заключение

Спасибо, что дочитали до конца! Надеюсь, вы используете некоторые из описанных здесь техник в своей следующей библиотеке (на Rust).