Содержание


Rust - новый язык программирования: Часть 15. Модульная система и крэйты. Обзор стандартной библиотеки

Comments

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

Идея разделения программ на модули существует уже давно. Поэтому практически все современные языки программирования (и особенно компилируемые) поддерживают модульную систему. О преимуществах модульного подхода при создании программного обеспечения говорить не приходится - всё уже многократно подтверждено, как в теории, так и на практике. Rust предоставляет в распоряжение разработчика модульную систему, основанную на крэйтах.

1. Крэйты и файлы исходного кода

Авторы языка Rust достаточно часто подчёркивают тот факт, что Rust является _компилируемым_ языком. Это не просто констатация очевидного, здесь главный смысл заключается в том, чтобы пользователь постоянно помнил о различении и даже о семантическом разделении следующих фаз: время компиляции (compile-time) и время выполнения (run-time). Семантические правила, которые имеют статическую интерпретацию (static interpretation), определяют успешное или неудачное завершение процесса компиляции. Эти правила авторы языка обозначили термином "статическая семантика" ("static semantics"). Семантические правила, обозначенные термином "динамическая семантика" ("dynamic semantics"), обусловливают поведение программ во время выполнения. Программа, компиляция которой завершилась неудачно из-за нарушения правила времени компиляции, не имеет определённой динамической семантики; компилятор должен остановить (экстренно прервать) процесс компиляции с сообщением об ошибке и не создавать выполняемый объект.

Модель компиляции сконцентрирована на объектах, именуемых крэйтами (crates). Каждый процесс компиляции обрабатывает одиночный крэйт в форме исходного кода, и если компиляция завершается успешно, то результатом её становится один отдельный крэйт в бинарной форме: либо выполняемый файл, либо библиотека.

Крэйт - это единица компиляции и связывания (linking), а кроме того, системы контроля версий, дистрибуции и загрузки во время выполнения. Крэйт содержит дерево (tree) вложенных областей видимости модулей. Верхний уровень этого дерева - модуль, который является анонимным (без имени) (с точки зрения путей внутри этого модуля), а любой элемент внутри крэйта имеет канонический путь (module path), обозначающий его местоположение в дереве модулей данного крэйта.

Компилятор Rust всегда вызывается с указанием единственного файла с исходным кодом в качестве входного и всегда генерирует единственный выходной крэйт. Обработка такого исходного файла может приводить к тому, что будут загружаться другие исходные файлы, как модули. Файлы с исходным кодом обычно имеют расширение .rs, но по соглашению исходные файлы, которые представляют крэйты, имеют расширение .rc и называются крэйт-файлами (crate files).

Файл исходного кода Rust описывает модуль, имя и расположение которого - в дереве модулей текущего крэйта - определяются извне относительно данного исходного файла: либо по явному указанию mod_item в ссылочном исходном файле (referencing source file), либо по имени самого текущего крэйта.

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

Листинг 1. Пример определения атрибутов для крэйта
// Linkage attributes
#[ link(name = "projx",
        vers = "2.5",
        uuid = "9cccc5d5-aceb-4af5-8285-811211826b82") ];

// Additional metadata attributes
#[ desc = "Project X" ];
#[ license = "BSD" ];
#[ author = "Jane Doe" ];

// Specify the output type
#[ crate_type = "lib" ];

// Turn on a warning
#[ warn(non_camel_case_types) ];

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

2. Элементы и атрибуты

Крэйты содержат элементы (items), каждый из которых может иметь некоторое количество атрибутов (attributes), связанных с конкретным элементом.

2.1 Элементы

Элемент (item) - компонент крэйта; некоторые элементы-модули могут быть определены в крэйт-файлах, но всё же большинство определяется в файлах исходного кода.

Возможные виды элементов (items) - модули, функции, определения типа, структуры, перечисления, статические элементы (static items), трэйты (traits), реализации (implementations) и внешние блоки - можно описать следующей схемой:

item : mod_item | fn_item | type_item | struct_item | enum_item | static_item
     | trait_item | impl_item | extern_block ;

Элементы организованы внутри крэйта в форме набора вложенных модулей (modules). Каждый крэйт имеет единственный "самый внешний" анонимный (безымянный) модуль; все прочие элементы внутри этого крэйта обладают путевыми именами (path) в дереве модулей данного крэйта.

Элементы (items) полностью определяются во время компиляции, но как правило остаётся возможность их корректирования во время выполнения, и они могут размещаться в памяти, защищённой от записи (read-only).

Некоторые элементы формируют неявную область видимости (scope) для объявления подэлементов (sub-items). Другими словами, объявления элементов в функции или в модуле могут быть (во многих случаях) смешаны с инструкциями, управляющими блоками и прочими подобными сущностями, образующими тело рассматриваемого элемента. Смысл этих элементов (и подэлементов) с ограниченной областью видимости тот же самый, как если бы элемент был объявлен вне области видимости: он остаётся статическим элементом (static item) - за исключением того, что путевое имя (path name) этого элемента внутри пространства имён (namespace) модуля квалифицируется именем включающего (содержащего) его элемента или является скрытым (private) внутри включающего его элемента (как в случае объектов, объявляемых внутри тела функции). Грамматика языка определяет точные места, в которых могут появляться определения таких подэлементов.

2.2 Параметры-типы

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

Все элементы за исключением модулей могут быть параметризованы (parameterized) по типу. Параметры-типы, как и в случае обобщённых функций, задаются в виде разделённого запятыми списка идентификаторов, заключённого в угловые скобки <...,...,...>, следующего после имени элемента, но перед его определением. Параметры-типы любого элемента считаются "частью имени" элемента, но не частью его типа. Ссылочное путевое имя элемента должно (в принципе) предоставлять типы аргументов, как список разделённых запятыми типов, заключённый в угловые скобки, для того чтобы обратиться именно к конкретному параметризованному по типу элементу. На практике система логического вывода типа обычно может вывести эти необходимые типы аргументов из контекста. Обобщённых параметризованных типов не существует, только элементы, параметризованные по типу. Таким образом, в Rust отсутствует синтаксическая форма записи для абстракции типа: здесь нет суперкласса-прародителя "универсального для всех" типов.

2.3. Модули

Формальное представление модуля:

mod_item : "mod" ident ( ';' | '{' mod '}' );
mod : [ view_item | item ] * ;

Модуль представляет собой контейнер для нуля или более элементов, управляющих областью видимости (view items; подробнее о них будет сказано ниже), и нуля или более элементов (items). Элементы области видимости, как понятно из их названия, управляют видимостью элементов (подэлементов), определяемых внутри данного модуля, а также видимостью имён из области за пределами данного модуля при обращении к ним изнутри этого модуля.

Элемент модуль (module item) - модуль, взятый в фигурные скобки, снабжённый именем и префиксом в виде ключевого слова mod. Элемент модуль представляет новый именованный модуль, включаемый в дерево модулей, образующее крэйт. Модули могут быть вложенными произвольным образом. Пример модуля приведён в листинге 2.

Листинг 2. Пример определения модуля
mod math {
  type complex = (f64, f64);
  fn sin( f: f64 ) -> f64 {
    ...
  }
  fn cos( f: f64 ) -> f64 {
    ...
  }
  fn tan( f: f64 ) -> f64 {
    ...
  }
}

Модули и типы совместно используют одно и то же пространство имён. Объявление именованного типа, имеющего то же имя, что и модуль в области видимости, запрещено: таким образом, определение типа, трэйт, структура, перечисление или параметр-тип не могут замещать имя модуля в области видимости и наоборот, имя модуля не может заместить имя типа.

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

Листинг 3. Пример загрузки обычного модуля и вложенного модуля
// загрузка модуля 'vec' из файла 'vec.rs'
mod vec;

mod task {
  // загрузка вложенного модуля 'local_data' из файла 'task/local_data.rs'
  mod local_data;
}

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

Листинг 4. Определение и изменение пути загрузки с помощью атрибута path
#[path = "task_files"]
mod task {
  // Load the 'local_data' module from 'task_files/tls.rs'
  #[path = "tls.rs"]
  mod local_data;
}

2.4. Элементы, управляющие областью видимости

Элемент, управляющий областью видимости (view item), управляет пространством имён модуля. Эти элементы не определяют новые элементы, они просто изменяют видимость других элементов.

view_item : extern_mod_decl | use_decl ;

Из формального представления элементов, управляющих областью видимости, очевидно, что такими элементами могут быть объявления внешних модулей extern_mod_decl и объявления use_decl.

2.4.1. Объявления внешних модулей

Формальное представление объявления внешнего модуля:

extern_mod_decl : "extern" "mod" ident [ '(' link_attrs ')' ] ? ;
link_attrs : link_attr [ ',' link_attrs ] + ;
link_attr : ident '=' literal ;

Объявление extern mod (внешнего модуля) определяет зависимость от внешнего крэйта. После объявления этот внешний крэйт привязывается к области видимости (включается в область видимости), в которой было сделано данное объявление, как идентификатор ident, указанный в extern_mod_decl.

Объявленный таким образом внешний крэйт сводится к заданному so-имени (динамическая so-библиотека в данном контексте) во время компиляции, а требование динамического связывания с этим so-именем (динамической библиотекой) во время выполнения передаётся линкеру для загрузки во время выполнения. So-имя определяется во время компиляции посредством сканирования путей к библиотекам, заданных для компилятора, и посредством сопоставления атрибутов link_attrs, предоставленных в объявлении use_decl, с любыми атрибутами #link, которые были объявлены в рассматриваемом внешнем крэйте в то время, когда он сам компилировался. Если атрибуты link_attrs не заданы, то по умолчанию назначается (присваивается) атрибут name, эквивалентный атрибуту ident, заданному в объявлении use_decl. Примеры объявления внешних модулей показаны в листинге 5.

Листинг 5. Три примера объявлений внешнего модуля (extern mod)
extern mod pcre (uuid = "54aba0f8-a7b1-4beb-92f1-4cf625264841");
extern mod extra;  // equivalent to: extern mod extra ( name = "extra" );
extern mod rustextra (name = "extra");  // linking to 'extra' under another name

2.4.2. Объявления use

Формальное представление объявления:

use_decl : "pub"? "use" ident [ '=' path | "::" path_glob ] ;
path_glob : ident [ "::" path_glob ] ?
          | '*'
          | '{' ident [ ',' ident ] * '}'

Объявление use создаёт одну или несколько локальных привязок имён-синонимов, связанных с некоторым другим путевым именем (path). Обычно объявление use используется для сокращения длины путевого имени, требуемого для обращения к элементу-модулю.

Здесь следует обратить особое внимание на то, что, в отличие от многих языков программирования, объявления use в Rust _не_ _объявляют_ зависимости для связывания с внешними крэйтами. Вместо этого объявления внешних модулей (extern mod) объявляют зависимости для связывания (для работы линкера).

Объявления use поддерживают несколько удобных сокращений:

a) Перепривязка целевого имени, как нового локального имени с использованием синтаксиса

use x = p::q::r;

b) Одновременная привязка списка путевых имён, отличающихся только конечным фрагментом с использованием синтаксиса glob-like с фигурными скобками

use a::b::{c,d,e,f};

c) Привязка всех путевых имён, соответствующих заданному префиксу, с использованием синтаксиса с применением шаблонного символа "звёздочка"

use a::b::*

Некоторые варианты объявлений use продемонстрированы в листинге 6.

Листинг 6. Примеры объявлений use
use std::float::sin;
use std::option::{Some, None};

fn main() {
  // equivalent to 'info!( std::float::sin(1.0) );'
  info!( sin(1.0) );

  // equivalent to 'info!( ~[std::option::Some(1.0), std::option::None] );'
  info!( ~[Some(1.0), None] );
}

Как и любые другие элементы, объявления use по умолчанию являются защищёнными, скрытыми (private) в содержащем их модуле. Но так же, как и любые другие элементы, объявление use может быть открытым для общего доступа (public), если объявление квалифицировано ключевым словом pub. Такое объявление use служит для реэкспорта имени, как, например, показано ниже в листинге 7. Следовательно, объявление use с открытым доступом может перенаправлять (redirect) некоторое общедоступное (public) имя в другое (отличающееся от исходного) целевое определение: даже определение с закрытым (private) каноническим путём, расположенное внутри другого модуля. Если последовательность таких перенаправлений образует цикл или не может быть разрешено однозначно, то это является ошибкой времени компиляции.

Листинг 7. Пример открытого объявления use для реэкспорта имени
mod engulfing {
  pub use engulfing::shallow::*;

  pub mod shallow {
    pub fn pick() { }
    pub fn pack() { }
  }
}

В этом примере модуль engulfing реэкспортирует все открытые (public) имена, определённые в модуле shallow.

Также следует отметить, что пути, содержащиеся в элементах use, являются относительными по отношению к корню соответствующего крэйта. Так в предыдущем примере из листинга 7 use ссылается на engulfing::shallow::*, а не просто на shallow::*.

2.5. Практический пример определения и компиляции крэйтов

На основании приведённых выше описаний вполне можно прийти к выводу о том, что модульная система языка Rust достаточно сложна. Для того, чтобы несколько упростить понимание этого механизма, следует обратиться к небольшому примеру, в котором определяются два крэйта: world.rs (см. листинг 8), который представляет библиотеку для второго крэйта - main.rs (см. листинг 9).

Листинг 8. Крэйт world.rs
// world.rs
#[link(name = "world", vers = "0.01")];
pub fn explore() -> &'static str { "всем людям" }
Листинг 9. Крэйт main.rs
// main.rs
extern mod world;
fn main() {
  println( "Мир " + world::explore() );
}

Процесс компиляции и выполнения показан в листинге 10. В первой строке выполняется компиляция динамической библиотеки, которая получает имя libworld-<hash-значение>-0.01.so. Во второй строке компилируется программа main, использующая скомпилированную перед этим динамическую библиотеку, расположенную в текущем каталоге (обозначен точкой). Далее программа выполняется.

Листинг 10. Компиляция двух крэйтов и выполнение программы
$ rustc --lib world.rs
$ rustc main.rs -L .
$ ./main
"Мир всем людям"

2.6. Краткий обзор стандартной библиотеки

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

Может возникнуть вопрос: почему возможно использование функции println() и ей подобных без каких-либо объявлений и директив в исходных кодах программ? Ответ прост: компилятор rustc автоматически вставляет в корневой крэйт предлагаемой ему программы строку

extern mod std;

Кроме того, в тело каждого модуля компилятор добавляет строку

use std::prelude::*;

которая выполняет реэкспортирование всех общих определений из std.

Это позволяет без какого бы то ни было дополнительного импортирования непосредственно использовать типы и функции языка Rust, такие как Option<T> и пресловутая println(). Но не все элменты Rust включены в prelude, поэтому в некоторых случаях всё же потребуется явное импортирование с помощью команды use.

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

Листинг 11. Реэкспорт функции println() из std::io
use puts = std::io::println;
fn main() {
  println( "println импортируется по умолчанию." );
  puts( "Но можно импортировать её вручную под другим именем." );
  ::std::io::println( "Или просто не пользоваться автоматическим импортированием." );
}

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

#[no_std];

а в любом другом модуле с помощью директивы

#[no_implicit_prelude];

В стандартной библиотеке определены все простые и составные типы языка Rust, их свойства и методы. Кроме того, в стандартную библиотеку включены определения всех аллокаторов памяти, управляемых, собственных, заимствованных и небезопасных (unsafe) указателей, определения специализированных типов (option и result), средства управления многозадачностью (task), средства обмена данными (communication), зависимые от платформ сущности (os и path), средства ввода/вывода (io), контейнеры, трэйты общего назначения (kinds, ops, cmp, num, to_str, clone), а также интерфейс со стандартной библиотекой языка C (libc). Документацию по стандартной библиотеке можно найти на сайте языка Rust в разделе Standard Library.

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

extern mod extra;

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

Заключение

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=952248
ArticleTitle=Rust - новый язык программирования: Часть 15. Модульная система и крэйты. Обзор стандартной библиотеки
publish-date=11082013