Неожиданно тёплый приём, оказанный публикой Хабра моему посту о самодельном компиляторе XD Pascal для MS-DOS, заставил меня задуматься. Не досадно ли, что любительский проект, которому я отдал немало сил, лежит у меня мёртвым грузом с тех самых пор, как из Windows полностью исчезла виртуальная машина DOS? Итогом размышлений стал компилятор XD Pascal для Windows. Возможно, он лишился некоторой доли ностальгического шарма и утратил возможность наивной работы с графикой через прерывания BIOS. Однако переход на Windows вдохнул новую жизнь в проект и открыл дорогу к давней мечте — самокомпиляции.
Как и прежде, никакими вспомогательными инструментами для автоматической генерации компиляторов я не пользовался. Такое упрямство может выглядеть странным, однако проект имел единственную цель — моё собственное удовольствие, и дополнительные инструменты послужили бы здесь лишь помехой. В этом смысле компилятор разрабатывался с чистого листа.
Стоит сказать несколько слов об основных задачах, которые пришлось решить на пути от DOS к Windows:
Формирование заголовков и секций исполняемого файла. Помимо официального описания формата Portable Executable, отличным подспорьем на этом этапе стала статья Creating the smallest possible PE executable. Поскольку в заголовках и секциях требуются точные адреса процедур и переменных, а их можно узнать лишь после вычисления размера кода и глобальных данных, то компиляцию пришлось сделать трёхпроходной. При первом проходе строится граф вызовов процедур и отмечаются «мёртвые» процедуры; при втором — вычисляются адреса, размер кода и данных, заполняются заголовки; при третьем — генерируется код. Такой кунштюк весьма неизящен, особенно с учётом того, что при каждом проходе заново повторяются все этапы компиляции, начиная с лексического разбора. Однако он приводит к очень лаконичному исходному коду компилятора и не требует никакого промежуточного представления программы. Дополнение: в настоящее время реализована генерация перемещаемого кода, компиляция сделана однопроходной.
Новый генератор кода. Компиляция для Windows потребовала заменить пары регистров «сегмент — смещение» на 32-битные регистры смещений, а также удалить (а местами добавить) префиксы изменения длины операнда (66h) и длины адреса (67h).
Директива для объявления внешних функций Windows API. Все имена функций, объявленных с директивой
Удаление множеств и инфиксных строковых операций из исходного кода. Это требование связано с задачей самокомпиляции. Вычисление любых выражений в XD Pascal строится так, что все промежуточные результаты имеют длину 32 бита и сохраняются в стеке. Для строк и множеств Паскаля этот подход неприемлем. Точнее, он позволил бы иметь множества размером до 32 элементов, но такие множества оказались бы практически бесполезны. Дополнение: теперь реализована поддержка строковых операций и множеств размером до 256 элементов.
Обёртки для некоторых процедур. Идея самокомпиляции привела к обёртыванию вызовов некоторых процедур стандартной библиотеки. Сигнатура обёртки едина для случаев компиляции внешним компилятором (Delphi/Free Pascal) и самокомпиляции; обёртываемые процедуры при этом различаются. Таким образом, вся специфика способа компиляции локализуется внутри нескольких обёрток. Паскаль пестрит процедурами, которые при ближайшем рассмотрении оказывается невозможно реализовать по правилам самого Паскаля:
Задача самокомпиляции, несмотря на своё символическое значение, остаётся ограниченной: компилятор является консольной программой и поэтому выглядит не совсем полноправным обитателем мира Windows. Понадобилось ещё несколько нововведений на пути к компиляции программ с оконным интерфейсом:
Директива компилятору для установки типа интерфейса. Тип интерфейса (консольный или графический) должен быть указан в отдельном поле заголовка исполняемого файла. Как известно, в Delphi и Free Pascal для этого существует директива
Операция взятия адреса процедур и функций. В классическом Паскале нет полноценных указателей на процедуры и функции — их в некоторой мере заменяет процедурный тип. Этот тип в XD Pascal не реализован. Как бы то ни было, до сих пор применение операции
Явное указание имён подключаемых библиотек. Для консольных программ было достаточно импорта функций Windows API из библиотеки
Демонстрационная программа с GUI
В результате получился очень простой самокомпилируемый компилятор для Windows. Вряд ли корректно сравнивать его с могучими коллективными проектами типа Free Pascal. Скорее он попадает в весовую категорию известного любительского BeRo Tiny Pascal. По сравнению с ним XD Pascal имеет заметные преимущества: более строго соблюдается грамматика Паскаля и контролируются ошибки, есть полноценный файловый ввод/вывод, поддерживается арифметика чисел с плавающей точкой, нет зависимости от внешнего ассемблера, допускается компиляция программ с оконным интерфейсом.
Далее мне предстоит разобраться с ложноположительным срабатыванием некоторых антивирусов — новой проблемой, о которой я и не задумывался в маленьком уютном мирке MS-DOS. Если повезёт, XD Pascal будет внедрён, наряду с BeRo Tiny Pascal, в лабораторный практикум по курсу конструирования компиляторов в МГТУ им. Н.Э. Баумана.
Как и прежде, никакими вспомогательными инструментами для автоматической генерации компиляторов я не пользовался. Такое упрямство может выглядеть странным, однако проект имел единственную цель — моё собственное удовольствие, и дополнительные инструменты послужили бы здесь лишь помехой. В этом смысле компилятор разрабатывался с чистого листа.
Пять шагов к самокомпиляции в Windows
Стоит сказать несколько слов об основных задачах, которые пришлось решить на пути от DOS к Windows:
Формирование заголовков и секций исполняемого файла. Помимо официального описания формата Portable Executable, отличным подспорьем на этом этапе стала статья Creating the smallest possible PE executable. Поскольку в заголовках и секциях требуются точные адреса процедур и переменных, а их можно узнать лишь после вычисления размера кода и глобальных данных, то компиляцию пришлось сделать трёхпроходной. При первом проходе строится граф вызовов процедур и отмечаются «мёртвые» процедуры; при втором — вычисляются адреса, размер кода и данных, заполняются заголовки; при третьем — генерируется код. Такой кунштюк весьма неизящен, особенно с учётом того, что при каждом проходе заново повторяются все этапы компиляции, начиная с лексического разбора. Однако он приводит к очень лаконичному исходному коду компилятора и не требует никакого промежуточного представления программы. Дополнение: в настоящее время реализована генерация перемещаемого кода, компиляция сделана однопроходной.
Новый генератор кода. Компиляция для Windows потребовала заменить пары регистров «сегмент — смещение» на 32-битные регистры смещений, а также удалить (а местами добавить) префиксы изменения длины операнда (66h) и длины адреса (67h).
Директива для объявления внешних функций Windows API. Все имена функций, объявленных с директивой
external
, заносятся в таблицы секции импорта исполняемого файла. Поскольку эти функции требуют передачи аргументов справа налево, пришлось вручную инвертировать порядок аргументов в объявлении и вызовах всех таких функций. Тем самым отпала необходимость в инверсии средствами компилятора. Ради простоты все аргументы процедур и функций в XD Pascal передаются в виде 32-битных величин; к счастью, это правило справедливо и для функций Windows API, так что взаимодействие с системными библиотеками не привело к усложнению механизма передачи аргументов. Дополнение: инверсия порядка аргументов импортируемых функций теперь выполняется автоматически.Удаление множеств и инфиксных строковых операций из исходного кода. Это требование связано с задачей самокомпиляции. Вычисление любых выражений в XD Pascal строится так, что все промежуточные результаты имеют длину 32 бита и сохраняются в стеке. Для строк и множеств Паскаля этот подход неприемлем. Точнее, он позволил бы иметь множества размером до 32 элементов, но такие множества оказались бы практически бесполезны. Дополнение: теперь реализована поддержка строковых операций и множеств размером до 256 элементов.
Обёртки для некоторых процедур. Идея самокомпиляции привела к обёртыванию вызовов некоторых процедур стандартной библиотеки. Сигнатура обёртки едина для случаев компиляции внешним компилятором (Delphi/Free Pascal) и самокомпиляции; обёртываемые процедуры при этом различаются. Таким образом, вся специфика способа компиляции локализуется внутри нескольких обёрток. Паскаль пестрит процедурами, которые при ближайшем рассмотрении оказывается невозможно реализовать по правилам самого Паскаля:
Read
, Write
, Move
и т.д. Для самых употребительных процедур, в том числе Read
и Write
, я сделал исключение и реализовал их нетипичными для грамматики языка, но привычными любому знатоку Паскаля. Для большинства остальных нетипичных процедур понадобились обёртки. Таким образом, XD Pascal не во всём совместим с Delphi или Free Pascal, однако большой беды в этом нет, поскольку даже сам Free Pascal в режиме совместимости с Delphi фактически остаётся несовместимым. Дополнение: ныне реализована поддержка нетипизированных формальных аргументов-переменных. Это позволило сделать процедуры BlockRead
, BlockWrite
, Move
, FillChar
совместимыми с Delphi и Free Pascal, тем самым радикально сократив количество необходимых обёрток. Компиляция программ с GUI
Задача самокомпиляции, несмотря на своё символическое значение, остаётся ограниченной: компилятор является консольной программой и поэтому выглядит не совсем полноправным обитателем мира Windows. Понадобилось ещё несколько нововведений на пути к компиляции программ с оконным интерфейсом:
Директива компилятору для установки типа интерфейса. Тип интерфейса (консольный или графический) должен быть указан в отдельном поле заголовка исполняемого файла. Как известно, в Delphi и Free Pascal для этого существует директива
$APPTYPE
. Аналогичная директива $A
появилась и в XD Pascal.Операция взятия адреса процедур и функций. В классическом Паскале нет полноценных указателей на процедуры и функции — их в некоторой мере заменяет процедурный тип. Этот тип в XD Pascal не реализован. Как бы то ни было, до сих пор применение операции
@
к процедурам в моём скромном проекте казалось мне бесполезным. Однако обработка событий Windows API построена на обратных вызовах (callbacks), и здесь передача адреса вызываемой процедуры-обработчика вдруг стала насущной необходимостью. Дополнение: в XD Pascal теперь добавлена полноценная поддержка процедурного типа.Явное указание имён подключаемых библиотек. Для консольных программ было достаточно импорта функций Windows API из библиотеки
KERNEL32.DLL
. Программы с GUI потянули за собой USER32.DLL
, GDI32.DLL
и т.д. Понадобилось расширить синтаксис директивы external
, добавив туда имя библиотеки.Демонстрационная программа с GUI
Что в итоге
В результате получился очень простой самокомпилируемый компилятор для Windows. Вряд ли корректно сравнивать его с могучими коллективными проектами типа Free Pascal. Скорее он попадает в весовую категорию известного любительского BeRo Tiny Pascal. По сравнению с ним XD Pascal имеет заметные преимущества: более строго соблюдается грамматика Паскаля и контролируются ошибки, есть полноценный файловый ввод/вывод, поддерживается арифметика чисел с плавающей точкой, нет зависимости от внешнего ассемблера, допускается компиляция программ с оконным интерфейсом.
Далее мне предстоит разобраться с ложноположительным срабатыванием некоторых антивирусов — новой проблемой, о которой я и не задумывался в маленьком уютном мирке MS-DOS. Если повезёт, XD Pascal будет внедрён, наряду с BeRo Tiny Pascal, в лабораторный практикум по курсу конструирования компиляторов в МГТУ им. Н.Э. Баумана.