diff --git a/11-расширение-масштаба/README.md b/11-расширение-масштаба/README.md index 2bafe63..a138934 100644 --- a/11-расширение-масштаба/README.md +++ b/11-расширение-масштаба/README.md @@ -82,7 +82,7 @@ import std.stdio; // Получить доступ к writeln и всему ос import widget; ``` -![]() +![image-11-1-1](images/image-11-1-1.png) ***Рис. 11.1.*** *Пример структуры каталога* @@ -119,7 +119,1661 @@ import goodies.io; [В начало ⮍](#11-1-1-объявления-import) [Наверх ⮍](#11-расширение-масштаба) +### 11.1.2. Базовые пути поиска модулей + +Анализируя объявление `import`, компилятор выполняет поиск не толь +ко относительно текущего каталога, где происходит компиляция. Ина +че невозможно было бы использовать ни одну из стандартных библио +тек или других библиотек, развернутых за пределами каталога текуще +го проекта. В конце концов мы постоянно включаем модули из пакета +`std`, хотя в поле зрения наших проектов нет никакого подкаталога `std`. +Как же работает этот механизм? + +Как и многие другие языки, D позволяет задать набор *базовых путей* +(*roots*), откуда начинается поиск модулей. С помощью аргумента, пере +даваемого компилятору из командной строки, к списку базовых путей +поиска модулей можно добавить любое количество каталогов. Точный +синтаксис этой операции зависит от компилятора; эталонный компи +лятор `dmd` использует флаг командной строки `-I`, сразу за которым ука +зывается путь, например `-Ic:\Programs\dmd\src\phobos` для Windows-вер +сии и `-I/usr/local/src/phobos` для UNIX-версии. С помощью дополнитель +ных флагов `-I` можно добавить любое количество путей в список путей +поиска. + +Например, при анализе объявления `import path.to.file` сначала подката +лог `path/to`[^5] ищется в текущем каталоге. Если такой подкаталог сущест +вует, запрашивается файл `file.d`. Если файл найден, поиск завершает +ся. В противном случае такой же поиск выполняется, начиная с каждо +го из базовых путей, заданных с помощью флага `-I`. Поиск завершается +при первом нахождении модуля; если же все каталоги пройдены безре +зультатно, компиляция прерывается с ошибкой «модуль не найден». + +Если компонент `path.to` отсутствует, поиск модуля будет осуществлять +ся непосредственно в базовых каталогах. + +Для пользователя было бы обременительно добавлять флаг командной +строки только для того, чтобы получить доступ к стандартной библио +теке или другим широко используемым библиотекам. Вот почему эта +лонный компилятор (и фактически любой другой) использует простой +конфигурационный файл, содержащий несколько флагов командной +строки по умолчанию, которые автоматически добавляются к каждому +вызову компилятора из командной строки. Сразу после инсталляции +компилятора конфигурационный файл должен содержать такие уста +новки, чтобы с его помощью можно было найти, по крайней мере, биб +лиотеку поддержки времени исполнения и стандартную библиотеку. +Поэтому если вы просто введете + +```sh +% dmd main.d +``` + +то компилятор сможет найти все артефакты стандартной библиотеки, +не требуя никаких параметров в командной строке. Чтобы точно узнать, +где ищется каждый из модулей, можно при запуске компилятора `dmd` +добавить флаг `-v` (от verbose – подробно). Подробное описание того, как +установленная вами версия D загружает конфигурационные парамет +ры, вы найдете в документации для нее (в случае `dmd` документация раз +мещена в Интернете). + +### 11.1.3. Поиск имен + +Как ни странно, в D нет глобального контекста или глобального про +странства имен. В частности, нет способа определить истинно глобаль +ный объект, функцию или имя класса. Причина в том, что единствен +ный способ определить такую сущность – разместить ее в модуле, а у лю +бого модуля должно быть имя. В свою очередь, имя модуля порождает +именованный контекст. Даже `Object`, предок всех классов, в действи +тельности не является глобальным именем: на самом деле, это объект +`object.Object`, поскольку он вводится в модуле `object`, поставляемом по +умолчанию. Вот, например, содержимое файла `widget.d`: + +```d +// Содержимое файла widget.d +void fun(int x) +{ + ... +} +``` + +С определением функции `fun` не вводится глобально доступный иденти +фикатор `fun`. Вместо этого все, кто включает модуль `widget` (например, +файл `main.d`), получают доступ к идентификатору `widget.fun`: + +```d +// Содержимое main.d +import widget; + +void main() +{ + widget.fun(10); // Все в порядке, ищем функцию fun в модуле widget +} +``` + +Все это очень хорошо и модульно, но при этом довольно многословно +и неоправданно строго. Если нужна функция `fun` и никто больше ее не +определяет, почему компилятор не может просто отдать предпочтение +`widget.fun` как единственному претенденту? + +На самом деле, именно так и работает поиск имен. Каждый включае +мый модуль вносит свое пространство имен, но когда требуется найти +идентификатор, предпринимаются следующие шаги: + +1. Идентификатор ищется в текущем контексте. Если идентификатор +найден, поиск успешно завершается. +2. Идентификатор ищется в контексте текущего модуля. Если иденти +фикатор найден, поиск успешно завершается. +3. Идентификатор ищется во всех включенных модулях: + - если идентификатор не удается найти, поиск завершается неуда +чей; + - если идентификатор найден в единственном модуле, поиск ус +пешно завершается; + - если идентификатор найден более чем в одном модуле и этот иден +тификатор не является именем функции, поиск завершается +с ошибкой, выводится сообщение о дублировании идентифика +тора; + - если идентификатор найден более чем в одном модуле и этот иден +тификатор является именем функции, применяется механизм раз +решения имен при кроссмодульной перегрузке (см. раздел 5.5.2). + +Привлекательным следствием такого подхода является то, что клиент +ский код обычно может быть кратким, а многословным только тогда, +когда это действительно необходимо. В предыдущем примере функция +`main.d` могла вызвать `fun` проще, без каких-либо «украшений»: + +```d +// Содержимое main.d +import widget; + +void main() +{ + fun(10); // Все в порядке, идентификатор fun определен только в модуле widget +} +``` + +Пусть в файле `io.d` также определена функция `fun` с похожей сигнату +рой: + +```d +// Содержимое io.d из каталога acme/goodies +void fun(long n) +{ + ... +} +``` + +И пусть модуль с функцией `main` включает и файл `widget.d`, и файл `io.d`. +Тогда «неприукрашенный» вызов `fun` окажется ошибочным, но уточ +ненные вызовы с указанием имени модуля по-прежнему будут работать +нормально: + +```d +// Содержимое main.d +import widget, acme.goodies.io; + +void main() +{ + fun(10); // Ошибка! Двусмысленный вызов функции fun(): идентификатор fun найден в модулях widget и acme.goodies.io + widget.fun(10); // Все в порядке, точное указание + acme.goodies.io.fun(10); // Все в порядке, точное указание +} +``` + +Обратите внимание: сама собой двусмысленность не проявляется. Если +вы не попытаетесь обратиться к идентификатору в двусмысленной фор +ме, компилятор никогда не пожалуется. + +#### 11.1.3.1. Кроссмодульная перегрузка функций + +В разделе 5.5.2 обсуждается вопрос перегрузки функций в случае их +расположения в разных модулях и приводится пример, в котором моду +ли, определяющие функцию с одним и тем же именем, вовсе не +обязательно порождают двусмысленность. Теперь, когда мы уже знаем +больше о модулях и модульности, пора поставить точку в этом разговоре. + +*Угон функций* (*function hijacking*) представляет собой особенно хитрое +нарушение модульности. Угон функций имеет место, когда функция +в некотором модуле состязается за вызовы из функции в другом модуле +и принимает их на себя. Типичное проявление угона функций: работаю +щий модуль ведет себя по-разному в зависимости от того, каковы другие +включенные модули, или от порядка, в котором эти модули включены. + +Угоны могут появляться как следствие непредвиденных эффектов в дру +гих случаях исправно выполняемых и благонамеренных правил. В ча +стности, кажется логичным, чтобы в предыдущем примере, где модуль +`widget` определяет `fun(int)`, а модуль `acme.goodies.io` – `fun(long)`, вызов +`fun(10)`, сделанный в `main`, был присужден функции `widget.fun`, посколь +ку это «лучший» вариант. Однако это один из тех случаев, когда луч +шее – враг хорошего. Если модуль с функцией `main` включает только +`acme.goodies.io`, то вызов `fun(10)`, естественно, отдается `acme.goodies.io.fun` +как единственному кандидату. Однако если на сцену выйдет модуль +`widget`, вызов `fun(10)` неожиданно переходит к `widget.fun`. На самом деле, +`widget` вмешивается в контракт, который изначально заключался меж +ду `main` и `acme.goodies.io` – ужасное нарушение модульности. + +Неудивительно, что языки программирования остерегаются угона. C++ +разрешает угон функций, но большинство руководств по стилю про +граммирования советуют этого приема избегать; а Python, как и мно +гие другие языки, и вовсе запрещает любой угон. С другой стороны, пе +реизбыток воздержания может привести к излишне строгим правилам, +воспитывающим привычку использовать в именах длинные строки +идентификаторов. + +D разрешает проблему угона оригинальным способом. Основной руково +дящий принцип подхода D к кроссмодульной перегрузке состоит в том, +что добавление или уничтожение включаемых модулей не должно вли +ять на разрешение имени функции. Возня с инструкциями `import` мо +жет привести к тому, что ранее компилируемые модули перестанут ком +пилироваться, а ранее некомпилируемые модули станут компилируе +мыми. Опасный сценарий, который D исключает, – тот, при котором, +поиграв с объявлениями `import`, вы оставите программу компилируе +мой, но с разными результатами разрешения имен при перегрузке. + +Для любого вызова функции, найденного в модуле, справедливо, что +если имя этой функции найдено более чем в одном модуле *и* если вызов +может сработать с версией функции из любого модуля, то такой вызов +ошибочен. Если же вызов можно заставить работать лишь при одном ва +рианте разрешения имени, такой вызов легален, поскольку при таких +условиях нет угрозы угона. + +В приведенном примере, где `widget` определяет `fun(int)`, а `acme.goodies.io` – +`fun(long)`, положение дел в модуле `main` таково: + +```d +import widget, acme.goodies.io; + +void main() +{ + fun(10); // Ошибка! Двусмысленная кроссмодульная перегрузка! + fun(10L); // Все в порядке, вызов недвусмысленно переходит к acme.goodies.io.fun + fun("10"); // Ошибка! Ничего не подходит! +} +``` + +Добавив или удалив из инструкции `import` идентификатор `widget` или +`acme.goodies.io`, можно заставить сломанную программу работать, или +сломать работающую программу, или оставить работающую програм +му работающей – но никогда с различными решениями относительно +вызовов `fun` в последнем случае. + +### 11.1.4. Объявления public import + +По умолчанию поиск идентификаторов во включаемых модулях не яв +ляется транзитивным. Рассмотрим каталог на рис. 11.1. Если модуль +main включает модуль widget, а модуль widget в свою очередь включает +модуль acme.gadget, то поиск идентификатора, начатый из main, в модуле +acme.gadget производиться не будет. Какие бы модули ни включал мо +дуль widget, это лишь деталь реализации модуля widget, и для main она +не имеет значения. +Тем не менее может статься, что модуль widget окажется лишь расшире +нием другого модуля или будет иметь смысл лишь в связке с другим мо +дулем. Например, определения из модуля widget могут использовать +и требовать так много определений из модуля acme.goodies.io, что для +любого другого модуля было бы бесполезно использовать widget, не +включив также и acme.goodies.io. В таких случаях вы можете помочь +клиентскому коду, воспользовавшись объявлением public import: +// Содержимое widget.d +// Сделать идентификаторы из acme.goodies.io видимыми всем клиентам widget +public import acme.goodies.io; +Данное объявление public import делает все идентификаторы, опреде +ленные модулем acme/goodies/io.d, видимыми из модулей, включающих +widget.d (внимание) как будто widget.d определил их сам. По сути, public +import добавляет в widget.d объявление alias для каждого идентифика +тора из io.d. (Дублирование кода объектов не происходит, только неко +торое дублирование идентификаторов.) Предположим, что модуль io.d +определяет функцию print(string), а в функцию main.d поместим сле +дующий код: +import widget; +void main() { +print("Здравствуй"); +// Все в порядке, идентификатор print найден +widget.print("Здравствуй"); // Все в порядке, widget фактически +// определяет print +} +Что если на самом деле включить в main и модуль acme.goodies.io? По +пробуем это сделать: +import widget; +import acme.goodies.io; // Излишне, но безвредно +void main() { +print("Здравствуй"); +// Все в порядке... +widget.print("Здравствуй"); +// ...в порядке... +acme.goodies.io.print("Здравствуй"); // ... и в порядке! +} +Модулю io.d вред не нанесен: тот факт, что модуль widget определяет +псевдоним для acme.goodies.io, ни в коей мере не влияет на исходный +идентификатор. Дополнительный псевдоним – это просто альтернатив +ное средство получения доступа к одному и тому же определению. +Наконец, в некотором более старом коде можно увидеть объявления +private import. Такая форма использования допустима и аналогична +обычному объявлению import. + +11.1.5. Объявления static import +Иногда добавление включаемого модуля в неявный список для поиска +идентификаторов при объявлении import (в соответствии с алгоритмом +из раздела 11.1.3) может быть нежелательным. Бывает уместным жела +ние осуществлять доступ к определенному в модуле функционалу толь +ко с явным указанием полного имени (а-ля имямодуля.имяидентификатора, +а не имяидентификатора). +Простейший случай, когда такое решение оправданно, – использование +очень популярного модуля в связке с модулем узкого назначения при +совпадении ряда идентификаторов в этих модулях. Например, в стан +дартном модуле std.string определены широко используемые функции +для обработки строк. Если вы взаимодействуете с устаревшей систе +мой, применяющей другую кодировку (например, двухбайтный набор +знаков, известный как DBCS – Double Byte Character Set), то захотите +использовать идентификаторы из std.string в большинстве случаев, +а идентификаторы из собственного модуля dbcs_string – лишь изредка +и с точным указанием. Для этого нужно просто указать в объявлении +import для dbcs_string ключевое слово static: +import std.string; +// Определяет функцию string toupper(string) +static import dbcs_string; // Тоже определяет функцию string toupper(string) +void main() { +auto s1 = toupper("hello"); +// Все в порядке +auto s2 = dbcs_string.toupper("hello"); // Все в порядке +} +Уточним: если бы этот код не включал объявление import std.string, +первый вызов просто не компилировался бы. Для static import поиск +идентификаторов не автоматизируется, даже когда идентификатор не +двусмысленно разрешается. +Бывают и другие ситуации, когда конструкция static import может быть +полезной. Сдержать автоматический поиск и использовать более много +словный, но одновременно и более точный подход может пожелать и мо +дуль, включающий множество других модулей. В таких случаях клю +чевое слово static полезно использовать с целыми списками значений, +разделенных запятыми: +static import teleport, time_travel, warp; +Или располагать его перед контекстом, заключенным в скобки, с тем +же результатом: +static { +import teleport; +import time_travel, warp; +} + +11.1.6. Избирательные включения +Другой эффективный способ справиться с конфликтующими иденти +фикаторами – включить лишь определенные идентификаторы из моду +ля. Для этого используйте следующий синтаксис: +// Содержимое main.d +import widget : fun, gun; +Избирательные включения обладают точностью хирургического лазе +ра: данное объявление import вводит ровно два идентификатора – fun +и gun. После избирательного включения невидим даже идентификатор +widget! Предположим, модуль widget определяет идентификаторы fun, +gun и hun. В таком случае fun и gun можно будет использовать только так, +будто их определил сам модуль main. Любые другие попытки, такие как +hun, widget.hun и даже widget.fun, незаконны: +// Содержимое main.d +import widget : fun, gun; +void main() { +fun(); +// Все в порядке +gun(); +// Все в порядке +hun(); +// Ошибка! +widget.fun(); // Ошибка! +widget.hun(); // Ошибка! +} +Высокая точность и контроль, предоставляемые избирательным вклю +чением, сделали это средство довольно популярным – есть программи +сты, не приемлющие ничего, кроме избирательного включения; особен +но много таких среди тех, кто прежде работал с языками, обладающи +ми более слабыми механизмами включения и управления видимостью. +И все же необходимо отметить, что другие упомянутые выше механиз +мы уничтожения двусмысленности, которые предоставляет D, ничуть +не менее эффективны. Полный контроль над включаемыми идентифи +каторами был бы гораздо более полезен, если бы механизм поиска иден +тификаторов, используемый D по умолчанию, не был безошибочным. + +11.1.7. Включения с переименованием +Большие проекты имеют тенденцию создавать запутанные иерархии +пакетов. Чрезмерно ветвистые структуры каталогов – довольно частый +артефакт разработки, особенно в проектах, где заранее вводят щедрую, +всеобъемлющую схему именования, способную сохранить стабильность +даже при непредвиденных добавлениях в проект. Вот почему нередки +ситуации, когда модулю приходится использовать очень глубоко вло +женный модуль: +import util.container.finite.linear.list; +В таких случаях может быть весьма полезно включение с переименова +нием, позволяющее присвоить сущности util.container.finite.linear.list +короткое имя: +import list = util.container.finite.linear.list; +С таким объявлением import программа может использовать идентифи +катор list.symbol вместо чересчур длинного идентификатора util.con +tainer.finite.linear.list.symbol. Если исходить из того, что модуль, о ко +тором идет речь, определяет класс List, в итоге получим: +import list = util.container.finite.linear.list; +void main() { +auto lst1 = new list.List; +// Все в порядке +auto lst2 = new util.container.finite.linear.list.List; // Ошибка! +// Идентификатор util не определен! +auto lst3 = new List; +// Ошибка! +// Идентификатор List не определен! +} +Включение с переименованием не делает видимыми переименованные +пакеты (то есть util, container, …, list), так что попытка использовать ис +ходное длинное имя в определении lst2 завершается неудачей при поис +ке первого же идентификатора util. Кроме того, включение с переимено +ванием, без сомнения, обладает статической природой (см. раздел 11.1.5) +в том смысле, что не использует механизм автоматического поиска; вот +почему не вычисляется выражение new List. Если вы действительно хо +тите не только переименовать идентификаторы, но еще и сделать их ви +димыми, очень удобно использовать конструкцию alias (см. раздел 7.4): +import util.container.finite.linear.list; +// Нестатическое включение +alias util.container.finite.linear.list list; // Для удобства +void main() { +auto lst1 = new list.List; +// Все в порядке +auto lst2 = new util.container.finite.linear.list.List; // Все в порядке +auto lst3 = new List; +// Все в порядке +} +Переименование также может использоваться в связке с избирательны +ми включениями (см. раздел 11.1.6). Продемонстрируем это на примере: +import std.stdio : say = writeln; +void main() { +say("Здравствуй, мир!"); +// Все в порядке, вызвать writeln +std.stdio.say("Здравствуй, мир"); +// Ошибка! +writeln("Здравствуй, мир!"); +// Ошибка! +std.stdio.writeln("Здравствуй, мир!"); // Ошибка! +} +Как и ожидалось, применив избирательное включение, которое одно +временно еще и переименовывает идентификатор, вы делаете видимым +лишь включаемый идентификатор и ничего больше. +Наконец, можно переименовать и модуль, и включаемый идентифика +тор (включаемые идентификаторы): +import io = std.stdio : say = writeln, CFile = File; +Возможные взаимодействия между двумя переименованными включен +ными идентификаторами могли бы вызвать некоторые противоречия. +Язык D решил этот вопрос, просто сделав предыдущее объявление тож +дественным следующим: +import io = std.stdio : writeln, File; +import std.stdio : say = writeln, CFile = File; +Дважды переименовывающее объявление import эквивалентно двум дру +гим объявлениям. Первое из этих объявлений переименовывает только +модуль, а второе – только включаемый идентификатор. Таким образом, +новая семантика определяется в терминах более простых, уже извест +ных видов инструкции import. Предыдущее определение вводит иден +тификаторы io.writeln, io.File, say и CFile. + +11.1.8. Объявление модуля +Как говорилось в разделе 11.1, по той простой причине, что import при +нимает лишь идентификаторы, пакеты и модули на D, которые предпо +лагается хоть когда-либо включать в другие модули с помощью этой +конструкции, должны иметь имена, являющиеся допустимыми иден +тификаторами языка D. +В отдельных ситуациях требуется, чтобы модуль замаскировался име +нем, отличным от имени файла, где расположен код модуля, и притво +рился бы, что путь до пакета, которому принадлежит модуль, отлича +ется от пути до каталога, где на самом деле располагается упомянутый +файл. Очевидная ситуация, когда это может понадобиться: имя модуля +не является допустимым идентификатором D. +Предположим, вы пишете программу, которая придерживается более +широкого соглашения по именованию, предписывающего использовать +дефисы в имени файла, например gnome-cool-app.d. Тогда компилятор D +откажется компилировать ее, даже если сама программа будет полно +стью корректной. И все потому, что во время компиляции D должен ге +нерировать информацию о каждом модуле, каждый модуль должен об +ладать допустимым именем, а gnome-cool-app не является таковым. Про +стой способ обойти это правило – хранить исходный код под именем +gnome-cool-app, а на этапе сборки переименовывать его, например в gnome_ +cool_app.d. Этот трюк, конечно, сработает, но есть способ проще и луч +ше: достаточно вставить в начало файла объявление модуля, которое +выглядит так: +module gnome_cool_app; +Если такое объявление присутствует в gnome-cool-app.d (но обязательно +в качестве первого объявления в файле), то компилятор будет доволен, +поскольку он генерирует всю информацию о модуле, используя имя +gnome_cool_app. В таком случае истинное имя вообще никак не проверя +ется; в объявлении модуля имя может быть хоть таким: +module path.to.nonexistent.location.app; +Тогда компилятор сгенерирует всю информацию о модуле, как будто он +называется app.d и расположен в каталоге path/to/nonexistent/location. +Компилятору все равно, потому что он не обращается по этому адресу: +поиск файлов ассоциируется исключительно с import, а здесь, при непо +средственной компиляции gnome-cool-app.d, никаких включений нет. + +11.1.9. Резюме модулей +Язык D поощряет модель разработки, которая не требует отделения объ +явлений от сущностей, определяемых программой (в C и C++ эти поня +тия фигурируют как «заголовки» и «исходные коды»). Вы просто рас +полагаете код в модуле и включаете этот модуль в другие с помощью +конструкции import. Тем не менее иногда хочется принять другую мо +дель разработки, предписывающую более жесткое разделение между +сигнатурами, которые модуль должен реализовать, и кодом, который +стоит за этими сигнатурами. В этом случае потребуется работать с так +называемыми резюме модулей (module summaries), построенными на +основе исходного кода. Резюме модуля – это минимум того, что необхо +димо знать модулю о другом модуле, чтобы использовать его. +Резюме модуля – это фактически модуль без комментариев и реализа +ций функций. Реализации функций, использующих параметры време +ни компиляции, тем не менее в резюме модуля остаются. Ведь функции +с параметрами времени компиляции должны быть доступны во время +компиляции, так как могут быть вызваны непредвиденным образом +в модуле-клиенте. +Резюме модуля состоит из корректного кода на D. Например: +/** +Это документирующий комментарий для этого модуля +*/ +module acme.doitall; +/** +Это документирующий комментарий для класса A +*/ +class A { +void fun() { ... } +final void gun() { ... } +} +class B(T) { +void hun() { ... } +} +void foo() { +... +} +void bar(int n)(float x) { +... +} +При составлении резюме модуля doitall этот модуль копируется, но ис +ключаются все комментарии, а тела всех функций заменяются на ; (ис +ключение составляют функции с параметрами времени компиляции – +такие функции остаются нетронутыми): +module acme.doitall; +class A { +void fun(); +final void gun(); +} +class B(T) { +void hun() { ... } +} +void foo(); +void bar(int n)(float x) { +... +} +Резюме содержит информацию, необходимую другому модулю, чтобы +использовать acme.doitall. В большинстве случаев резюме модулей ав +томатически вычисляются внутри работающего компилятора. Но ком +пилятор может сгенерировать резюме по исходному коду и по вашему +запросу (в случае эталонной реализации компилятора dmd для этого пред +назначен флаг -H). Сгенерированные резюме полезны, когда вы, к приме +ру, хотите распространить библиотеку в виде заголовков плюс скомпи +лированная библиотека. +Заметим, что исключение тел функций все же не гарантировано. Ком +пилятор волен оставлять тела очень коротких функций в целях инлай +нинга. Например, если функция acme.doitall.foo обладает пустым те +лом или просто вызывает другую функцию, ее тело может присутство +вать в сгенерированном интерфейсном файле. +Подход к разработке, хорошо знакомый программистам на C и C++, за +ключается в сопровождении заголовочных файлов (то есть резюме) +и файлов с реализациями вручную и по отдельности. Если вы изберете +этот способ, работать придется несколько больше, но зато вы сможете +поупражняться в коллективном руководстве. Например, право изме +нять заголовочные файлы может быть закреплено за командой проек +тировщиков, контролирующей все детали интерфейсов, которые моду +ли предоставляют друг другу. А команду программистов, реализующих +эти интерфейсы, можно наделить правом изменять файлы реализации +и правом на чтение (но не изменение) заголовочных файлов, используе +мых в качестве текущей документации, направляющей процесс реали +зации. Компилятор проверяет, соответствует ли реализация интерфей +су (ну, по крайней мере синтаксически). +С языком D у вас есть выбор – вы можете: 1) вообще обойтись без резюме +модулей, 2) разрешить компилятору сгенерировать их за вас, 3) сопро +вождать модули и резюме модулей вручную. Все примеры в этой книге +выбирают вариант 1) – не использовать резюме модулей, оставив все +заботы компилятору. Чтобы опробовать две другие возможности, вам +сначала потребуется организовать модули так, чтобы их иерархия соот +ветствовала изображенной на рис. 11.2. + +![image-11-1-9](images/image-11-1-9.png) + +Рис. 11.2. Структура каталога для отделения резюме модулей +(«заголовков») от файлов реализации + +Чтобы использовать пакет acme, потребуется добавить родительский ка +талог каталогов acme и acme_impl к базовым путям поиска модулей про +екта (см. раздел 11.1.2), а затем включить модули из acme в клиентский +код с помощью следующих объявлений: +// Из модуля client.d +import acme.algebra; +import acme.io.network; +Каталог acme включает только файлы резюме. Чтобы заставить файлы +реализации взаимодействовать, необходимо, чтобы в качестве префик +са в именах соответствующих модулей фигурировал пакет acme, а не +acme_impl. Вот где приходят на помощь объявления модулей. Даже не +смотря на то, что файл algebra.d находится в каталоге acme_impl, вклю +чив следующее объявление, модуль algebra может заявить, что входит +в пакет acme: +// Из модуля acme_impl/algebra.d +module acme.algebra; +Соответственно модули в подпакете io будут использовать объявление: +// Из модуля acme_impl/io/file.d +module acme.io.file; +Эти строки позволят компилятору сгенерировать должные имена паке +тов и модулей. Чтобы во время сборки программы компилятор нашел +тела функций, просто передайте ему файлы реализации: +% dmd client.d /path/to/acme_impl/algebra.d +Директива import в client.d обнаружит интерфейсный файл acme.di в ка +талоге /path/to/acme. А компилятор найдет файл реализации точно там, +где указано в командной строке, с корректными именами пакета и мо +дуля. +Если коду из client.d потребуется использовать множество модулей из +пакета acme, станет неудобно указывать все эти модули в командной +строке компилятора. В таких случаях лучший вариант – упаковать весь +код пакета acme в бинарную библиотеку и передавать dmd только ее. Син +таксис для сборки библиотеки зависит от реализации компилятора; ес +ли вы работаете с эталонной реализацией, вам потребуется сделать что- +то типа этого: +% cd /path/to/acme_impl +% dmd -lib -ofacme algebra.d gui.d io/file.d io/network.d +Флаг -lib предписывает компилятору собрать библиотеку, а флаг -of (от +output file – файл вывода) направляет вывод в файл acme.lib (Windows) +или acme.a (UNIX-подобные системы). Чтобы клиентский код мог рабо +тать с такой библиотекой, нужно ввести что-то вроде: +% dmd client.d acme.lib +Если библиотека acme широко используется, ее можно сделать одной из +библиотек, которые проект использует по умолчанию. Но тут уже мно +гое зависит от реализации компилятора и от операционной системы, +так что для успеха операции придется прочесть это жуткое руководство. + +11.2. Безопасность +Понятие безопасности языков программирования всегда было противо +речивым, но за последние годы его определение удивительно кристал +лизовалось. +Интуитивно понятно, что безопасный язык тот, который «защищает +свои собственные абстракции» [46, гл. 1]. В качестве примера таких аб +стракций D приведем класс: +class A { int x; } +и массив: +float[] array; +По правилам языка D (тоже «абстракция», предоставляемая языком) +изменение внутреннего элемента x любого объекта типа A не должно из +менять какой-либо элемент массива array, и наоборот, изменение array[n] +для некоторого n не должно изменять элемент x некоторого объекта ти +па A. Как ни благоразумно запрещать такие бессмысленные операции, +в D есть способы заставить их обе выполниться – формируя указатели +с помощью cast или задействуя union. +void main() { +float[] array = new float[1024]; +auto obj = cast(A) array.ptr; +... +} +Изменение одного из элементов массива array (какого именно, зависит +от реализации компилятора, но обычно второго или третьего) изменяет +obj.x. + +11.2.1. Определенное и неопределенное поведение +Кроме только что приведенного примера с сомнительным приведением +указателя на float к ссылке на класс есть и другие ошибки времени ис +полнения, свидетельствующие о том, что язык нарушил определенные +обещания. Хорошими примерами могут послужить разыменование ука +зателя null, деление на ноль, а также извлечение вещественного квад +ратного корня из отрицательного числа. Никакая корректная програм +ма не должна когда-либо выполнять такие операции, и тот факт, что +они все же могут иметь место в программе, типы которой проверяются, +можно рассматривать как несостоятельность системы типов. +Проблема подобного критерия корректности, который «хорошо было +бы принять»: список ошибок бесконечно пополняется. D сводит свое по +нятие безопасности к очень точному и полезному определению: безопас +ная программа на D характеризуется только определенным поведени +ем. Различия между определенным и неопределенным поведением: +• определенное поведение: выполнение фрагмента программы в задан +ном состоянии завершается одним из заранее определенных исхо +дов; один из возможных исходов – резкое прекращение выполнения +(именно это происходит при разыменовании указателя null и при де +лении на ноль); +• неопределенное поведение: эффект от выполнения фрагмента про +граммы в заданном состоянии не определен. Это означает, что может +произойти все, что угодно в пределах физических возможностей. +Хороший пример – только что упомянутый случай с cast: програм +ма с такой «раковой клеткой» некоторое время может продолжать +работу, но наступит момент, когда какая-нибудь запись в array с по +следующим случайным обращением к obj приведет к тому, что ис +полнение выйдет из-под контроля. +(Неопределенное поведение перекликается с понятием недиагностиро +ванных ошибок, введенным Карделли [15]. Он выделяет две большие +категории ошибок времени исполнения: диагностированные и недиаг +ностированные ошибки. Диагностированные ошибки вызывают немед +ленный останов исполнения, а недиагностированные – выполнение про +извольных команд. В программе с определенным поведением никогда +не возникнет недиагностированная ошибка.) +У противопоставления определенного поведения неопределенному есть +пара интересных нюансов. Рассмотрим, к примеру, язык, определяю +щий операцию деления на ноль с аргументами типа int, так что она +должна всегда порождать значение int.max. Такое условие переводит де +ление на ноль в разряд определенного поведения – хотя данное опреде +ление этого действия и нельзя назвать полезным. Примерно в том же +ключе std.math в действительности определяет, что операция sqrt(-1) +должна возвращать double.nan. Это также определенное поведение, по +скольку double.nan – вполне определенное значение, которое является +частью спецификации языка, а также функции sqrt. Даже деление на +ноль – не ошибка для типов с плавающей запятой: этой операции забот +ливо предписывается возвращать или плюс бесконечность, или минус +бесконечность, или NaN («нечисло») (см. главу 2). Результаты выполне +ния программ всегда будут предсказуемыми, когда речь идет о функ +ции sqrt или делении чисел с плавающей запятой. +Программа безопасна, если она не порождает неопределенное поведение. + +11.3.2. Атрибуты @safe, @trusted и @system +Нехитрый способ гарантировать отсутствие недиагностированных оши +бок – просто запретить все небезопасные конструкции D, например осо +бые случаи применения выражения cast. Однако это означало бы невоз- +можность реализовать на D многие системы. Иногда бывает очень нуж +но переступить границы абстракции, например, рассматривать область +памяти, имеющей некоторый тип, как область памяти с другим типом. +Именно так поступают менеджер памяти и сборщик мусора. В задачи +языка D всегда входила способность выразить логику такого программ +ного обеспечения на системном уровне. +С другой стороны, многие приложения нуждаются в небезопасном до +ступе к памяти лишь в сильно инкапсулированной форме. Язык может +заявить о том, что он безопасен, даже если его сборщик мусора реализо +ван на небезопасном языке. Ведь с точки зрения безопасного языка нет +возможности использовать сборщик небезопасным образом. Сборщик +сам по себе инкапсулирован внутри библиотеки поддержки времени ис +полнения, реализован на другом языке и воспринимается безопасным +языком как волшебный примитив. Любой недостаток безопасности +сборщика мусора был бы проблемой реализации языка, а не клиентско +го кода. +Как может большой проект обеспечить безопасность большинства своих +модулей, в то же время обходя правила в некоторых избранных случаях? +Подход D к безопасности – предоставить пользователю право самому +решать, чего он хочет: вы можете на уровне объявлений заявить, при +держивается ли ваш код правил безопасности или ему нужна возмож +ность переступить ее границы. Обычно информация о свойствах моду +ля указывается сразу же после объявления модуля, как здесь: +module my_widget; +@safe: +... +В этом месте определяются атрибуты @safe, @trusted и @system, которые +позволяют модулю объявить о своем уровне безопасности. (Такой под +ход не нов; в языке Модула-3 применяется тот же подход, чтобы отли- +чить небезопасные и безопасные модули.) +Код, размещенный после атрибута @safe, обязуется использовать ин +струкции лишь из безопасного подмножества D, что означает: +• никаких преобразований указателей в неуказатели (например, int), +и наоборот; +• никаких преобразований между указателями, типы которых не име +ют отношения друг к другу; +• проверка границ при любом обращении к массиву; +• никаких объединений, включающих указатели, классы и массивы, +а также структуры, которые содержат перечисленные запрещенные +типы в качестве внутренних элементов; +• никаких арифметических операций с указателями; +• запрет на получение адреса локальной переменной (на самом деле, +требуется запрет утечки таких адресов, но отследить это гораздо +сложнее); +• функции должны вызывать лишь функции, обладающие атрибутом +@safe или @trusted; +• никаких ассемблерных вставок; +• никаких преобразований типа, лишающих данные статуса const, +immutable или shared; +• никаких обращений к каким-либо сущностям с атрибутом @system. +Иногда эти правила могут оказаться излишне строгими; например, +в стремлении избежать утечки указателей на локальные переменные +можно исключить из рядов безопасных программ очевидно корректные +программы. Тем не менее безопасное подмножество D (по прозвищу +SafeD) все же довольно мощное – целые приложения могут быть полно +стью написаны на SafeD. +Объявление или группа объявлений могут заявить, что им, напротив, +требуется низкоуровневый доступ. Такие объявления должны содер +жать атрибут @system: +@system: +void * allocate(size_t size); +void deallocate(void* p); +... +Атрибут @system действенно отключает все проверки, позволяя исполь +зовать необузданную мощь языка – на счастье или на беду. +Наконец, подход библиотек нередко состоит в том, что они предлагают +клиентам безопасные абстракции, подспудно используя небезопасные +средства. Такой подход применяют многие компоненты стандартной +библиотеки D. В таких объявлениях можно указывать атрибут @trusted. +Модулям без какого-либо атрибута доступен уровень безопасности, на +значаемый по умолчанию. Выбор уровня по умолчанию можно настро +ить с помощью конфигурационных файлов компилятора и флагов ко +мандной строки; точная настройка зависит от реализации компилятора. +Эталонная реализация компилятора dmd предлагает атрибут по умолча +нию @system; задать атрибут по умолчанию @safe можно с помощью фла +га командной строки -safe. +В момент написания этой книги SafeD находится в состоянии α-версии, +так что порой небезопасные программы проходят компиляцию, а без +опасные – нет, но мы активно работаем над решением этой проблемы. + +11.3. Конструкторы и деструкторы модулей +Иногда модулям требуется выполнить какой-то инициализирующий +код для вычисления некоторых статических данных. Сделать это мож +но, вставляя явные проверки («Были ли эти данные добавлены?») везде, +где осуществляется доступ к соответствующим данным. Если такой +подход неудобен/неэффективен, помогут конструкторы модулей. +Предположим, что вы пишете модуль, зависящий от операционной сис +темы, и поведение этого модуля зависит от флага. Во время компиляции +легко распознать основные платформы (например, «Я Mac» или «Я PC»), +но определять версию Windows придется во время исполнения. +Чтобы немного упростить задачу, условимся, что наш код различает +лишь ОС Windows Vista и более поздние или ранние версии относитель +но нее. Пример кода, определяющего вид операционной системы на эта +пе инициализации модуля: +private enum WinVersion { preVista, vista, postVista } +private WinVersion winVersion; +static this() { +OSVERSIONINFOEX info; +info.dwOSVersionInfoSize = OSVERSIONINFOEX.sizeof; +GetVersionEx(&info) || assert(false); +if (info.dwMajorVersion < 6) { +winVersion = WinVersion.preVista; +} else if (info.dwMajorVersion == 6 && info.dwMinorVersion == 0) { +winVersion = WinVersion.vista; +} else { +winVersion = WinVersion.postVista; +} +} +Этот геройский подвиг совершает конструктор модуля static this(). Та +кие конструкторы модулей всегда выполняются до main. Любой задан +ный модуль может содержать любое количество конструкторов. +В свою очередь, синтаксис деструкторов модулей предсказуем: +// На уровне модуля +static ~this() { +... +} +Статические деструкторы выполняются после того, как выполнение main +завершится каким угодно образом, будь то нормальный возврат или по +рождение исключения. Модули могут определять любое количество де +структоров модуля и свободно чередовать конструкторы и деструкторы +модуля. + +11.3.1. Порядок выполнения в рамках модуля +Порядок выполнения конструкторов модуля в рамках заданного моду +ля всегда соответствует последовательности расположения этих кон +структоров в модуле, то есть сверху вниз (лексический порядок). Поря +док выполнения деструкторов модуля – снизу вверх (обратный лексиче +ский порядок). +Если один из конструкторов модуля не сможет выполниться и породит +исключение, то не будет выполнена и функция main. Выполняются лишь +статические деструкторы, лексически расположенные выше отказавше +го конструктора модуля. Если не сможет выполниться и породит ис +ключение какой-либо деструктор модуля, остальные деструкторы вы +полнены не будут, а приложение прекратит свое выполнение, выведя +сообщение об ошибке в стандартный поток. + +11.3.2. Порядок выполнения +при участии нескольких модулей +Если модулей несколько, определить порядок вызовов сложнее. Эти пра +вила идентичны определенным для статических конструкторов классов +(см. раздел 6.3.6) и исходят из того, что модули, включаемые другими +модулями, должны инициализироваться первыми, а очищаться – по +следними. Вот правила, определяющие порядок выполнения статиче +ских конструкторов модулей модуль1 и модуль2: +• +• +• +• +• +конструкторы или деструкторы модулей определяются только в од +ном из модулей модуль1 и модуль2, тогда не нужно заботиться об упо +рядочивании; +модуль1 не включает модуль модуль2, а модуль2 не включает модуль1: +упорядочивание не регламентируется – любой порядок сработает, +поскольку модули не зависят друг от друга; +модуль1 включает модуль2: конструкторы модуля2 выполняются до кон +структоров модуля1, а деструкторы модуля2 – после деструкторов моду +ля1; +модуль2 включает модуль1: конструкторы модуля1 выполняются до +конструкторов модуля2, а деструкторы модуля1 – после деструкторов +модуля2; +модуль1 включает модуль2, а модуль2 вкючает модуль1: диагностируется +ошибка «циклическая зависимость» и выполнение прерывается на +этапе загрузки программы. +Проверка на циклическую зависимость модулей в настоящий момент +делается во время исполнения. Такие циклы можно отследить и во вре +мя компиляции или сборки, но это мало что дает: проблема проявляет +ся в том, что программа отказывается загружаться, и можно предполо +жить, что перед публикацией программа запускается хотя бы один раз. +Тем не менее чем раньше обнаружена проблема, тем лучше, так что +язык оставляет реализации возможность выявить это некорректное +состояние и сообщить о нем. + +11.4. Документирующие комментарии +Писать документацию скучно, а для программиста нет ничего страш +нее скуки. В результате документация обычно содержит скупые, непол +ные и устаревшие сведения. +Автоматизированные построители документации стараются вывести +максимум информации из чистого кода, отразив заслуживающие вни +мания отношения между сущностями. Тем не менее современным авто +матизированным построителям нелегко задокументировать высоко +уровневые намерения по реализации. Современные языки помогают им +в этом, предписывая использовать так называемые документирующие +комментарии – особые комментарии, описывающие, например, опре +деленную пользователем сущность. Языковой процессор (или сам ком +пилятор, или отдельная программа) просматривает комментарии вме +сте с кодом и генерирует документацию в одном из популярных форма +тов (таком как XML, HTML или PDF). +D определяет для документирующих комментариев спецификацию, +описывающую формат комментариев и процесс их преобразования в це +левой формат. Сам процесс не зависит от целевого формата; транслятор, +управляемый простым и гибким шаблоном (также определяемым поль +зователем), генерирует документацию фактически в любом заданном +формате. +Всеобъемлющее изучение системы трансляции документирующих ком +ментариев не входит в задачу этой книги. Замечу только, что вам не по +мешает уделить этому больше внимания; документация многих проек +тов на D, а также веб-сайт эталонной реализации компилятора и его +стандартной библиотеки полностью сгенерированы на основе докумен +тирующих комментариев D. + +11.5. Взаимодействие с C и C++ +Модули на D могут напрямую взаимодействовать с функциями C и C++. +Есть ограничение: к этим функциям не относятся обобщенные функ +ции С++, поскольку для этого компилятор D должен был бы включать +полноценный компилятор C++. Кроме того, схема расположения полей +класса D не совместима с классами C++, использующими виртуальное +наследование. +Чтобы вызвать функцию C или C++, просто укажите в объявлении функ +ции язык и не забудьте связать ваш модуль с соответствующими биб +лиотеками: +extern(C) int foo(char*); +extern(C++) double bar(double); +Эти объявления сигнализируют компилятору, что вызов генерируется +с соответствующими схемой расположения в стеке, соглашением о вы +зовах и кодировкой имен (также называемой декорированием имен – +name mangling), даже если сами функции D отличаются по всем или +некоторым из этих пунктов. +Чтобы вызвать функцию на D из программы на C или C++, просто до +бавьте в реализацию одно из приведенных выше объявлений: +extern(C) int foo(char*) { +... // Реализация +} +extern(C++) double bar(double) { +... // Реализация +} +Компилятор опять организует необходимое декорирование имен и ис +пользует соглашение о вызовах, подходящее для языка-клиента. То +есть эту функцию можно с одинаковым успехом вызывать из модулей +как на D, так и на «иностранных языках». + +11.5.1. Взаимодействие с классами C++[^6] +Как уже говорилось, D не способен отобразить классы C++ в классы D. +Это связано с различием реализаций механизма наследования в этих +языках. Тем не менее интерфейсы D очень похожи на классы C++, по +этому D реализует следующий механизм взаимодействия с классами +C++: +// Код на С++ +class Foo { +public: +virtual int method(int a, int b) { +return a + b; +} +}; +Foo* newFoo() { +return new Foo(); +} +void deleteFoo(Foo* obj) { +delete obj; +} +// Код на D +extern (C++) { +interface Foo { +int method(int, int); +} +Foo newFoo(); +void deleteFoo(Foo); +} +void main() { +auto obj = newFoo; +scope(exit) deleteFoo(obj); +assert(obj.method(2, 3) == 5); +} +Следующий код создает класс, реализующий интерфейс С++, и исполь +зует объект этого интерфейса в вызове внешней функции С++, прини +мающей в качестве аргумента указатель на объект класса С++ Foo. + +extern (C++) void call(Foo); +// В коде C++ эта функция должна быть определена как void call(Foo* f); +extern (C++) interface Foo { +int bar(int, int); +} +class FooImpl : Foo { +extern (C++) int bar(int a, int b) { +// ... +} +} +void main() { +FooImpl f = new FooImpl(); +call(f); +} + +11.6. Ключевое слово deprecated +Перед любым объявлением (типа, функции или данных) может распо +лагаться ключевое слово deprecated. Оно действует как класс памяти, +но нисколько не влияет собственно на генерацию кода. Вместо этого +deprecated лишь информирует компилятор о том, что помеченная им +сущность не предназначена для использования. Если такая сущность +все же будет использована, компилятор выведет предупреждение или +даже откажется компилировать, если он был запущен с соответствую +щим флагом (-w в случае dmd). +Ключевое слово deprecated служит для планомерной постепенной ми +грации от старых версий API к более новым версиям. Причисляя соот +ветствующие объявления к устаревшим, можно настроить компилятор +так, чтобы он или принимал, или отклонял объявления с префиксом +deprecated. Подготовив очередное изменение, отключите компиляцию +deprecated – ошибки точно укажут, где требуется ваше вмешательство, +что позволит вам шаг за шагом обновить код. + +11.7. Объявления версий +В идеальном мире, как только программа написана, ее можно запус +кать где угодно. А здесь, на Земле, то и дело что-то заставляет вносить +в программу изменения – другая версия библиотеки, сборка для особых +целей или зависимость от платформы. Чтобы помочь справиться с этим, +D определяет объявление версии version, позволяющее компилировать +код в зависимости от определенных условий. +Способ использования версии намеренно прост и прямолинеен. Вы или +устанавливаете версию, или проверяете ее. Сама версия может быть +или целочисленной константой, или идентификатором: +version = 20100501; +version = FinalRelease; +Чтобы проверить версию, напишите: +version(20100501) { +... // Объявления +} +version (PreFinalRelease) { +... // Объявления +} else version (FinalRelease) { +... // Другие объявления +} else { +... // Еще объявления +} +Если версия уже присвоена, «охраняемые» проверкой объявления ком +пилируются, иначе они игнорируются. Конструкция version может +включать блок else, назначение которого очевидно. +Установить версию можно лишь до того, как она будет прочитана. По +пытки установить версию после того, как она была задействована в про +верке, вызывают ошибку времени компиляции: +version (ProEdition) { +... // Объявления +} +version = ProEdition; // Ошибка! +Реакция такова, поскольку присваивания версий не предназначены +для того, чтобы версии изменять: версия должна быть одной и той же +независимо от того, на какой фрагмент программы вы смотрите. +Указывать версию можно не только в файлах с исходным кодом, но +и в командной строке компилятора (например, -version=123 или -versi +on=xyz в случае эталонной реализации компилятора dmd). Попытка уста +новить версию как в командной строке, так и в файле с исходным кодом +также приведет к ошибке. +Простота семантики version не случайна. Было бы легко сделать кон +струкцию version более мощной во многих отношениях, но очень скоро +она начала бы работать наперекор своему предназначению. Например, +управление версиями C с помощью связки директив #if/#elif/#else, без +условно, позволяет реализовать больше тактик в определении версий – +именно поэтому управление версиями в проекте на C обычно содержит +змеиный клубок условий, направляющих компиляцию. Конструкция +version языка D намеренно ограничена, чтобы с ее помощью можно бы +ло реализовать лишь простое, единообразное управление версиями. +Компиляторы, как водится, имеют множество предопределенных вер +сий, таких как платформа (например, Win32, Posix или Mac), порядок бай +тов (LittleEndian, BigEndian) и так далее. Если включено тестирование +модулей, автоматически задается проверка version(unittest). Особыми +идентификаторами времени исполнения __FILE__ и __LINE__ обознача +ются соответственно имя текущего файла и строка в этом файле. Пол +ный список определений version приведен в документации вашего ком +пилятора. + +11.8. Отладочные объявления +Отладочное объявление – это лишь особая версия с идентичным син +таксисом присваивания и проверки. Конструкция debug была определе +на специально для того, чтобы стандартизировать порядок объявления +отладочных режимов и средств. +Типичный случай использования конструкции debug: +module mymodule; +... +void fun() { +int x; +... +debug(mymodule) writeln("x=", x); +... +} +Чтобы отладить модуль mymodule, укажите в командной строке при ком +пиляции этого модуля флаг -debug=mymodule, и выражение debug(mymodule) +вернет true, что позволит скомпилировать код, «охраняемый» соответ +ствующей конструкцией debug. Если использовать debug(5), то «охраняе +мый» этой конструкцией код будет включен при уровне отладки >= 5. +Уровень отладки устанавливается либо присваиванием debug целочис +ленной константы, либо флагом компиляции. Допустимо также ис +пользовать конструкцию debug без аргументов. Код, следующий за та +кой конструкцией, будет добавлен, если компиляция запущена с фла +гом -debug. Как и в случае version, нельзя присваивать отладочной вер +сии идентификатор после того, как он уже был проверен. + +11.9. Стандартная библиотека D +Стандартная библиотека D, фигурирующая в коде под именем Phobos[^7], +органично развивалась вместе с языком. В результате она включает как +API старого стиля, так и новейшие библиотечные артефакты, исполь +зующие более современные средства языка. +Библиотека состоит из двух основных пакетов – core и std. Первый со +держит фундаментальные средства поддержки времени исполнения: +реализации встроенных типов, сборщик мусора, код для начала и завер +шения работы, поддержка многопоточности, определения, необходимые +для доступа к библиотеке времени исполнения языка C, и другие компо +ненты, связанные с перечисленными. Пакет std предоставляет функ +циональность более высокого уровня. Преимущество такого подхода +в том, что другие библиотеки можно надстраивать поверх core, а с паке +том std они будут лишь сосуществовать, не требуя его присутствия. +Пакет std обладает плоской структурой: большинство модулей распо +лагаются в корне пакета. Каждый модуль посвящен отдельной функ +циональной области. Информация о некоторых наиболее важных моду +лях библиотеки Phobos представлена в табл. 11.2. + +Таблица 11.2. Обзор стандартных модулей + +|Модуль|Описание| +|-|-| +|`std.algorithm`|Этот модуль можно считать основой мощнейшей способности +к обобщению, присущей языку. Вдохновлен стандартной биб +лиотекой шаблонов C++ (Standard Template Library, STL). Со +держит больше 70 важных алгоритмов, реализованных очень +обобщенно. Большинство алгоритмов применяются к струк +турированным последовательностям идентичных элементов. +В STL базовой абстракцией последовательности служит ите +ратор, соответствующий примитив D – диапазон, для которо +го краткого обзора явно недостаточно; полное введение в диа +пазоны D доступно в Интернете| +|`std.array`|Функции для удобства работы с массивами| +|`std.bigint`|Целое число переменной длины с сильно оптимизированной +реализацией| +|`std.bitmanip`|Типы и часто используемые функции для низкоуровневых би +товых операций| +|`std.concurrency`|Средства параллельных вычислений (см. главу 13)| +|`std.container`|Реализации разнообразных контейнеров| +|`std.conv`|Универсальный магазин, удовлетворяющий любые нужды по +преобразованиям. Здесь определены многие полезные функ +ции, такие как to и text| +|`std.datetime`|Полезные вещи, связанные с датой и временем| +|`std.file`|Файловые утилит ы. Зачаст ую этот мод уль манип улируе т +файлами целиком; например, в нем есть функция read, кото +рая считывает весь файл, при этом std.file.read и понятия не +имеет о том, что можно открывать файл и читать его малень +кими порциями (об этом заботится модуль std.stdio, см. далее)| +|`std.functional`|Примитивы для определения и композиции функций| +|`std.getopt`|Синтаксический анализ командной строки| +|`std.json`|Обработка данных в формате JSON| +|`std.math`|В высшей степени оптимизированные, часто используемые +математические функции| +|`std.numeric`|Общие числовые алгоритмы| +|`std.path`|Утилиты для манипуляций с путями к файлам| +|`std.random`|Разнообразные генераторы случайных чисел| +|`std.range`|Определения и примитивы классификации, имеющие отно +шение к диапазонам| +|`std.regex`|Обработчик регулярных выражений| +|`std.stdio`|Стандартные библиотечные средства ввода/вывода, построен +ные на основе библиотеки stdio языка C. Входные и выходные +файлы предоставляют интерфейсы в стиле диапазонов, благо +даря чему многие алгоритмы, определенные в модуле std.algo +rithm, могут работать непосредственно с файлами| +|`std.string`|Функции, специфичные для строк. Строки тесно связаны +с std.algorithm, так что модуль std.string, относительно не +большой по размеру, в основном лишь ссылается (определяя +псевдонимы) на части std.algorithm, применимые к строкам| +|`std.traits`|Качества типов и интроспекция| +|`std.typecons`|Средства для определения новых типов, таких как Tuple| +|`std.utf`|Функции для манипулирования кодировками UTF| +|`std.variant`|Объявление типа Variant, который является контейнером для +хранения значения любого типа. Variant – это высокоуровне +вый union| + +11.10. Встроенный ассемблер[^8] +Строго говоря, большую часть задач можно решить, не обращаясь к столь +низкоуровневому средству, как встроенный ассемблер, а те немногие за +дачи, которым без этого не обойтись, можно написать и скомпилировать +отдельно, после чего скомпоновать с вашей программой на D обычным +способом. Тем не менее встроенный в D ассемблер – очень мощное сред +ство повышения эффективности кода, и упомянуть его необходимо. Ко +нечно, в рамках одной главы невозможно всеобъемлюще описать язык +ассемблера, да это и не нужно – ассемблеру для популярных платформ +посвящено множество книг[^9]. Поэтому здесь мы приводим синтаксис +и особенности применения встроенного ассемблера D, а описание ис +пользуемых инструкций оставим специализированным изданиям. +К моменту написания данной книги компиляторы языка D существо +вали для платформ x86 и x86-64, соответственно синтаксис встроенно +го ассемблера определен пока только для этих платформ. + +11.10.1. Архитектура x86 +Инструкции ассемблера можно встроить в код, разместив их внутри +конструкции asm: +asm { +naked; +mov ECX, EAX; +mov EAX, [ESP+size_t.sizeof*1]; +mov EBX, [ESP+size_t.sizeof*2]; +L1: +mov DH, [EBX + ECX - 1]; +mov [EAX + ECX - 1], DH; +loop L1; +ret; +} +Внутри конструкции asm допустимы следующие сущности: +• инструкция ассемблера: +инструкция арг1, арг2, ..., аргn; +• +метка: +метка: +• +псевдоинструкция: +псевдоинструкция арг1, арг2, ..., аргn; +• +комментарии. +Каждая инструкция пишется в нижнем регистре. После инструкции +через запятую указываются аргументы. Инструкция обязательно за +вершается точкой с запятой. Несколько инструкций могут распола +гаться в одной строке. Метка объявляется перед соответствующей ин +струкцией как идентификатор метки с последующим двоеточием. Пе +реход к метке может осуществляться с помощью оператора goto вне бло +ка asm, а также с помощью инструкций семейства jmp и call. Аналогично +внутри блока asm разрешается использовать метки, объявленные вне +блоков asm. Комментарии в код на ассемблере вносятся так же, как +и в остальном коде на D, другой синтаксис комментариев недопустим. +Аргументом инструкции может быть идентификатор, объявленный вне +блока asm, имя регистра, адрес (с применением обычных правил адреса +ции данной платформы) или литерал соответствующего типа. Адреса +можно записывать так (все эти адреса указывают на одно и то же зна +чение): +mov EDX, 5[EAX][EBX]; +mov EDX, [EAX+5][EBX]; +mov EDX, [EAX+5+EBX]; +Также разрешается использовать любые константы, известные на эта +пе компиляции, и идентификаторы, объявленные до блока asm: +int* p = arr.ptr; +asm +{ +mov EAX, p[EBP]; +// Помещает в EAX значение p. +mov EAX, p; +// То же самое. +mov EAX, [p + 2*int.sizeof]; // Помещает в EAX второй +// элемент целочисленного массива. +} +Если размер операнда неочевиден, используется префикс тип ptr: +add [EAX], 3; +add [EAX], int ptr 3; +// Размер операнда 3 неочевиден. +// Теперь все ясно. +Префикс ptr можно использовать в сочетании с типами near, far, byte, +short, int, word, dword, qword, float, double и real. Префикс far ptr не ис +пользуется в плоской модели памяти D. По умолчанию компилятор ис +пользует byte ptr. Префикс seg возвращает номер сегмента адреса: +mov EAX seg p[EBP]; +Этот префикс также не используется в плоской модели кода. +Также внутри блока asm доступны символы: $, указывающий на адрес +следующей инструкции, и __LOCAL_SIZE, означающий количество байт +в локальном кадре стека. +Для доступа к полю структуры, класса или объединения следует помес +тить адрес объекта в регистр и использовать полное имя поля в сочета +нии с offsetof: +struct Regs +{ +uint eax, ebx, ecx, edx; +} +void pushRegs(Regs* p) +{ +asm { +push EAX; +mov EAX, p; +// Помещаем в p.ebx значение EBX +mov [EAX+Regs.ebx.offsetof], EBX; +// Помещаем в p.ecx значение ECX +mov [EAX+Regs.ecx.offsetof], ECX; +// Помещаем в p.edx значение EDX +mov [EAX+Regs.edx.offsetof], EDX; +pop EBX; +// Помещаем в p.eax значение EAX +mov [EAX+Regs.eax.offsetof], EBX; +} +} +Ассемблер x86 допускает обращение к следующим регистрам (имена +регистров следует указывать заглавными буквами): +AL AH AX EAXBP EBPES CS SS DS GS FS +BL BH BX EBXSP ESPCR0 CR2 CR3 CR4 +CL CH CX ECXDI EDIDR0 DR1 DR2 DR3 DR6 DR7 +DL DH DX EDX +STSI ESITR3 TR4 TR5 TR6 TR7 +ST(0) ST(1) ST(2) ST(3) ST(4) ST(5) ST(6) ST(7) +MM0 MM1 MM2 MM3 MM4 MM5 MM6 MM7 +XMM0 XMM1 XMM2 XMM3 XMM4 XMM5 XMM6 XMM7 +Ассемблер D вводит следующие псевдоинструкции: +align целочисленное_выражение; +целочисленное_выражение должно вычисляться на этапе компиляции. +align выравнивает следующую инструкцию по адресу, кратному це +лочисленному_выражению, вставляя перед этой инструкцией нужное ко +личество инструкций nop (от Not OPeration), имеющих код 0x90. +even; +Псевдоинструкция even выравнивает следующую инструкцию по +четному адресу (аналогична align 2). Выравнивание может сильно +повлиять на производительность в циклах, где часто выполняется +переход по выравниваемому адресу. +naked; +Псевдоинструкция naked указывает компилятору не генерировать +пролог и эпилог функции. В прологе, как правило, создается новый +кадр стека, а в эпилоге размещается код возвращения значения. Ис +пользуя naked, программист должен сам позаботиться о получении +нужных аргументов и возвращении результирующего значения в со +ответствии с применяемым функцией соглашением о вызовах. +Также ассемблер D разрешает вставлять в код непосредственные значе +ния с помощью псевдоинструкций db, ds, di, dl, df, dd, de, которые соот +ветствуют типам byte, short, int, long, float, double и extended и соответ +ственно размещают значения этого типа (extended – тип с плавающей +запятой длиной 10 байт, известный в D как real). Каждая такая псевдо +инструкция может иметь насколько аргументов. Строковый литерал +в качестве аргумента эквивалентен указанию n аргументов, где n – дли +на строки, а каждый аргумент соответствует одному знаку строки. +Следующий пример делает то же самое, что и первый пример в этом +разделе: +asm { +naked; +db 0x89, 0xc1, 0x8b, 0x44, 0x24, 0x04, 0x8b; +db 0x5c, 0x24, 0x08, 0x8a, 0x74, 0x0b, 0xff; +db 0x88, 0x74, 0x08, 0xff, 0xe2, 0xf6, 0xc3; // Коротко и ясно. +} +Префиксы инструкций, такие как lock, rep, repe, repne, repnz и repz, ука +зываются как отдельные псевдоинструкции: +asm +{ +rep; +movsb; +} +Ассемблер D не поддерживает инструкцию pause. Вместо этого следует +писать: +rep; +nop; +Для операций с плавающей запятой следует использовать формат с дву +мя аргументами. +fdiv ST(1); +fmul ST; +fdiv ST,ST(1); +fmul ST,ST(0); +// Неправильно +// Неправильно +// Правильно +// Правильно + +11.10.2. Архитектура x86-64 +Архитектура x86-64 является дальнейшим развитием архитектуры х86 +и в большинстве случаев сохраняет обратную совместимость с ней. Рас +смотрим отличия архитектуры x86-64 от x86. +Регистры общего назначения в x86-64 расширены до 64 бит. Их имена: +RAX, RBX, RCX, RDX, RBP, RSI, RDI, RSP, RIP и RFLAGS, причем RIP теперь доступен +из ассемблерного кода. Вдобавок добавились восемь 64-разрядных ре +гистров общего назначения R8, R9, R10, R11, R12, R13, R14, R15. Для доступа +к младшим 32 битам такого регистра к названию добавляется суф +фикс D, к младшим 16 – W, к младшим 8 – B. Так, R8D – младшие 4 байта +регистра R8, а R15B – младший байт R15. Также добавились восемь XMM- +регистров XMM8–XMM15. +Рассмотрим регистр RIP подробнее. Регистр RIP всегда содержит указа +тель на следующую инструкцию. Если в архитектуре х86, чтобы полу +чить адрес следующей инструкции, приходилось писать код вида: +asm +{ +call $; +// Поместить в стек адрес следующей инструкции +// и передать на нее управление. +pop EBX; +// Вытолкнуть адрес возврата в EBX. +add EBX, 6; +// Скорректировать адрес на размер +// инструкций pop, add и mov. +mov AL, [EBX]; // Теперь AL содержит код инструкции nop; +nop; +} +то в x86-64 можно просто написать[^10]: +asm +{ +mov AL, [RIP]; // Загружаем код следующей инструкции. +nop; +} +К сожалению, выполнить переход по содержащемуся в RIP адресу с по +мощью jmp/jxx или call нельзя, равно как нельзя получить значение RIP, +скопировав его в регистр общего назначения или стек. Впрочем, call $; +как раз помещает в стек адрес следующей инструкции, что, по сути, +идентично push RIP; (если бы такая инструкция была допустима). По +дробную информацию можно найти в официальном руководстве по +конкретному процессору. + +11.10.3. Разделение на версии +По своей природе ассемблерный код является платформозависимым. +Для х86 нужен один код, для x86-64 – другой, для SPARC – третий, +а компилятор для виртуальной машины вообще может не иметь встроен +ного ассемблера. Хорошая практика – реализовать требуемую функ +циональность без использования ассемблера, добавив альтернативные +реализации, оптимизированные для конкретных архитектур. Здесь +пригодится механизм версий. +Компилятор dmd определяет версию D_InlineAsm_X86, если доступен ас +семблер х86, и D_InlineAsm_X86_64 если доступен ассемблер x86-64. +Вот пример такого кода: +void optimizedFunction(void* arg) { +version(D_InlineAsm_X86) { +asm { +naked; +mov EBX, [EAX]; +} +} +else +version(D_InlineAsm_X86_64) { +asm { +naked; +mov RBX, [RAX]; +} +} +else { +size_t s = *cast(size_t*)arg; +} +} + +11.10.4. Соглашения о вызовах +Все современные парадигмы программирования основаны на процедур +ной модели. Каким бы ни был ваш код – функциональным, объектно- +ориентированным, агентно-ориентированным, многопоточным, распре +деленным, – он все равно будет вызывать процедуры. Разумеется, с по +вышением уровня абстракции, добавлением новых концепций процесс +вызова процедур неизбежно усложняется. +Процедурный подход выгоден при организации взаимодействия фраг +ментов программы, написанных на разных языках. Во-первых, разные +языки поддерживают разные парадигмы программирования, а во-вто +рых, даже одни и те же парадигмы могут быть реализованы по-разно +му. Между тем процедурный подход является тем самым фундаментом, +на котором основано все остальное. Этот фундамент надежен, стандар +тизирован и проверен временем. +Вызов процедуры, как правило, состоит из следующих операций: +• передача аргументов; +• сохранение адреса возврата; +• переход по адресу процедуры; +• выполнение процедуры; +• +• +передача возвращаемого значения; +переход по сохраненному адресу возврата. +В высокоуровневом коде знать порядок выполнения этих операций не +обязательно, однако при написании кода на ассемблере их придется +реализовывать самостоятельно. +То, как именно выполняются эти действия, определяется соглашения +ми о вызовах процедур. Их относительно немного, они хорошо стандар +тизированы. Разные языки используют разные соглашения о вызовах, +но, как правило, допускают возможность использовать несколько со +глашений. Соглашения о вызовах определяют, как передаются аргу +менты (через стек, через регистры, через общую память), порядок пере +дачи аргументов, значение каких регистров следует сохранять, как пе +редавать возвращаемое значение, кто возвращает указатель стека на +исходную позицию (вызывающая или вызываемая процедура). В сле +дующих разделах перечислены основные из этих соглашений. + +11.10.4.1. Соглашения о вызовах архитектуры x86 +Архитектура x86 за долгие годы своего существования породила мно +жество соглашений о вызовах процедур. У каждого из них есть свои +преимущества и недостатки. Все они требуют восстановления значений +сегментных регистров. +cdecl +Данное соглашение принято в языке C, отсюда и его название (C Decla +ration). Большинство языков программирования допускают использо +вание этого соглашения, и с его помощью наиболее часто организуется +взаимодействие подпрограмм, написанных на разных языках. В язы +ке D оно объявляется как функция с атрибутом extern(C). Аргументы +передаются через стек в обратном порядке, то есть начиная с последне +го. Последним в стек помещается адрес возврата. Значение возвращает +ся в регистре EAX, если по размеру оно меньше 4 байт, и на вершине сте +ка, если его размер превышает 4 байта. В этом случае значение в EAX +указывает на него. Если вы используете псевдоинструкцию naked, вам +придется обрабатывать переданные аргументы вручную. +extern(C) int increment(int a) { +asm { +naked; +mov EAX, [ESP+4]; // Помещаем в EAX значение a, смещенное на размер +// указателя (адреса возврата) от вершины стека. +inc EAX; +// Инкрементируем EAX +ret; +// Передаем управление вызывающей подпрограмме. +// Возвращаемое значение находится в EAX +} +} +Стек восстанавливает вызывающая подпрограмма. +pascal +Соглашение о вызовах языка Паскаль в D объявляется как функция +с атрибутом extern(Pascal). Аргументы передаются в прямом порядке, +стек восстанавливает вызываемая процедура. Значение возвращается +через передаваемый неявно первый аргумент. +stdcall +Соглашение операционной системы Windows, используемое в WinAPI. +Объявление: extern(Windows). Аналогично cdecl, но стек восстанавлива +ет вызываемая подпрограмма. +fastcall +Наименее стандартизированное и наиболее производительное соглаше +ние о вызовах. Имеет две разновидности – Microsoft fastcall и Borland +fastcall. В первом случае первые два аргумента в прямом порядке пере +даются через регистры ECX и EDX. Остальные аргументы передаются че +рез стек в обратном порядке. Во втором случае через регистры EAX, EDX +и ECX передаются первые три аргумента в прямом порядке, остальные ар +гументы передаются через стек в обратном порядке. В обоих случаях, +если размер аргумента больше размера регистра, он передается через +стек. Компиляторы D на данный момент не поддерживают данное согла +шение, однако при использовании динамических библиотек есть воз +можность получить указатель на такую функцию и вызвать ее с помо +щью встроенного ассемблера. +thiscall +Данное соглашение обеспечивает вызов методов класса в языке С++. +Полностью аналогично stdcall. Указатель на объект, метод которого +вызывается, передается через ECX. +Соглашение языка D +Функция D гарантирует сохранность регистров EBX, ESI, EDI, EBP. +Если данная функция имеет постоянное количество аргументов, пере +менное количество гомогенных аргументов или это шаблонная функ +ция с переменным количеством аргументов, аргументы передаются +в прямом порядке и стек очищает вызываемая процедура. (В противном +случае аргументы передаются в обратном порядке, после чего передает +ся аргумент _arguments. _argptr не передается, он вычисляется на базе +_arguments. Стек в этом случае очищает вызывающая процедура.) После +этого в стеке резервируется пространство под возвращаемое значение, +если оно не может быть возвращено через регистр. Последним передает +ся аргумент this, если вызываемая процедура – метод структуры или +класса, или указатель на контекст, если вызываемая процедура – деле +гат. Последний аргумент передается через регистр EAX, если он умеща +ется в регистр, не является трехбайтовой структурой и не относится +к типу с плавающей запятой. Аргументы ref и out передаются как ука +затель, lazy – как делегат. +Возвращаемое значение передается так: +• bool, byte, ubyte, short, ushort, int, uint, 1-, 2- и 4-байтовые структуры, +указатели (в том числе на объекты и интерфейсы), ссылки – в EAX; +• long, ulong, 8-байтовые структуры – в EDX (старшая часть) и EAX (млад +шая часть); +• float, double, real, ifloat, idouble, ireal – в ST0; +• cfloat, cdouble, creal – в ST1 (действительная часть) и ST0 (мнимая +часть); +• динамические массивы – в EDX (указатель) и EAX (длина массива); +• ассоциативные массивы – в EAX; +• делегаты – в EDX (указатель на функцию) и EAX (указатель на кон +текст). +В остальных случаях аргументы передаются через скрытый аргумент, +размещенный на стеке. В EAX в этом случае помещается указатель на +этот аргумент. + +11.10.4.2. Соглашения о вызовах архитектуры x86-64 +С переходом к архитектуре x86-64 количество соглашений о вызовах су +щественно сократилось. По сути, осталось только два соглашения о вы +зовах – Microsoft x64 calling convention для Windows и AMD64 ABI convention +для Posix. +Microsoft x64 calling convention +Это соглашение о вызовах очень напоминает fastcall. Аргументы пере +даются в прямом порядке. Первые 4 целочисленных аргумента переда +ются в RCX, RDX, R8, R9. Аргументы размером 16 байт, массивы и строки +передаются как указатель. Первые 4 аргумента с плавающей запятой +передаются через XMM0, XMM1, XMM2, XMM3. При этом место под эти аргумен +ты резервируется в стеке. Остальные аргументы передаются через стек. +Стек очищает вызывающая процедура. Возвращаемое значение переда +ется в RAX, если оно умещается в 8 байт и не является числом с плаваю +щей запятой. Число с плавающей запятой возвращается в XMM0. Если +возвращаемое значение больше 64 бит, память под него выделяет вызы +вающая процедура, передавая ее как скрытый аргумент. В этом случае +в RAX возвращается указатель на этот аргумент. +AMD64 ABI convention +Данное соглашение о вызовах используется в Posix-совместимых опера +ционных системах и напоминает предыдущее, однако использует боль +ше регистров. Для передачи целых чисел и адресов используются реги +стры RDI, RSI, RDX, RCX, R8 и R9, для передачи чисел с плавающей запятой – +XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6 и XMM7. Если требуется передать ар +гумент больше 64 бит, но не больше 256 бит, он передается по частям +через регистры общего назначения. В отличие от Microsoft x64, для пере +данных в регистрах аргументов место в стеке не резервируется. Возвра +щаемое значение передается так же, как и в Microsoft x64. + +11.10.5. Рациональность +Решив применить встроенный ассемблер для оптимизации программы, +следует понимать цену повышения эффективности. Ассемблерный код +трудно отлаживать, еще труднее сопровождать. Ассемблерный код об +ладает плохой переносимостью. Даже в пределах одной архитектуры +наборы инструкций разных процессоров несколько различаются. Более +новый процессор может предложить более эффективное решение стоя +щей перед вами задачи. А раз уж вы добиваетесь максимальной произ +водительности, то, возможно, предпочтете скомпилировать несколько +версий своей программы для различных целевых архитектур, напри +мер одну переносимую версию, использующую только инструкции из +набора i386, другую – для процессоров AMD, третью – для Intel Core. +Для обычного высокоуровневого кода достаточно просто указать соот +ветствующий флаг компиляции[^11], а вот в случае с ассемблером придет +ся создавать несколько версий кода, делающих одно и то же, но разны +ми способами. +version(AMD) +{ +version = i686; +version = i386; +} +else version(iCore) +{ +version = i686; +version = i386; +} +else version(i686) +{ +version = i386; +} +void fastProcess() +{ +version(AMD) { +// ... +} else version(iCore) { +// ... +} else version(i686) { +// ... +} else version(i386) +{ +// ... +} else { +// ... +} +} +И все это ради того, чтобы выжать из функции fastProcess максимум +производительности! Тут-то и надо задаться вопросом: а в самом ли деле +эта функция является краеугольным камнем вашей программы? Мо +жет быть, ваша программа недостаточно производительна из-за ошиб +ки на этапе проектирования, и выбор другого решения позволит сэконо +мить секунды процессорного времени – против долей миллисекунд, +сэкономленных на оптимизации fastProcess? А может, время и, как +следствие, деньги, которых требует написание ассемблерного кода, луч +ше направить на повышение производительности целевой машины? +В любом случае задействовать встроенный ассемблер для повышения +производительности нужно в последнюю очередь, когда остальные +средства уже испробованы. + [^1]: Прямой порядок байтов – от старшего к младшему байту. – *Прим. пер.* [^2]: Обратный порядок байтов – от младшего к старшему байту. – *Прим. пер.* [^3]: «Shebang» – от англ. *sharp-bang* или *hash-bang*, произношение символов `#!` – *Прим. науч. ред.* [^4]: Текущие версии реализации позволяют включать модули на уровне классов и функций. – *Прим. науч. ред.* +[^5]: В тексте / используется в качестве обобщенного разделителя; необходимо понимать, что реа льный разделитель зависит от системы. +[^6]: Описание этой части языка не было включено в оригинал книги, но по +скольку данная возможность присутствует в текущих реализациях языка, +мы добавили ее описание в перевод. – Прим. науч. ред. +[^7]: Фобос (Phobos) – больший из двух спутников планеты Марс. «Марс» – изна +чальное название языка D (см. введение). Digital Mars (Цифровой Марс) – +компания, разработавшая язык D и эталонную реализацию языка – ком +пилятор dmd (от Digital Mars D). – Прим. науч. ред. +[^8]: Описание этой части языка не было включено в оригинал книги, но по +скольку эта возможность присутствует в текущих реализациях языка, мы +добавили ее описание в перевод. – Прим. науч. ред. +[^9]: Например, есть хороший учебник для вузов «Assembler» В. И. Юрова. – +Прим. науч. ред. +[^10]: Ассемблер dmd2.052 не поддерживает доступ к регистру RIP. Возможно, +данная функция появится позже. Ну а пока вместо mov AL, [RIP]; вы можете +написать мантру db 0x8A, 0x05; di 0x00000000;, тем самым сообщив свое же +лание на языке процессора. Помните: если транслятор не понимает некото +рые символы или инструкции, вы можете транслировать ассемблерный код +в машинный сторонним транслятором и вставить в свой ассемблерный код +числовое представление команды, воспользовавшись псевдоинструкциями +семейства db. – Прим. науч. ред. +[^11]: Компилятор dmd2.057 пока трудно назвать промышленным компилято +ром, поэтому упомянутого механизма в нем пока нет, а вот компилятор язы +ка C gcc предоставляет возможность указать целевую платформу. Это позво +ляет получить максимально эффективный машинный код для данной +платформы, при этом в код на языке C вносить изменения не нужно. Чита +телям, нуждающимся в компиляторах D, способных генерировать более оп +тимизированный код, следует обратить внимание на проекты GDC (GNU D +compiler) и LDC (LLVM D compiler) компиляторов D, построенных на базе +генераторов кода GCC и LLVM. – Прим. науч. ред. diff --git a/11-расширение-масштаба/images/image-11-1-1.png b/11-расширение-масштаба/images/image-11-1-1.png new file mode 100644 index 0000000..fc39a4d Binary files /dev/null and b/11-расширение-масштаба/images/image-11-1-1.png differ diff --git a/11-расширение-масштаба/images/image-11-1-9.png b/11-расширение-масштаба/images/image-11-1-9.png new file mode 100644 index 0000000..e53f6b8 Binary files /dev/null and b/11-расширение-масштаба/images/image-11-1-9.png differ