Alexander Zhirov 2 лет назад
Родитель
Сommit
d6684cdd8c

+ 1655 - 1
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. – Прим. науч. ред.

BIN
11-расширение-масштаба/images/image-11-1-1.png


BIN
11-расширение-масштаба/images/image-11-1-9.png