История с продолжением: собственный компилятор Паскаля для Windows с чистого листа

Неожиданно тёплый приём, оказанный публикой Хабра моему посту о самодельном компиляторе XD Pascal для MS-DOS, заставил меня задуматься. Не досадно ли, что любительский проект, которому я отдал немало сил, лежит у меня мёртвым грузом с тех самых пор, как из Windows полностью исчезла виртуальная машина DOS? Итогом размышлений стал компилятор XD Pascal для Windows. Возможно, он лишился некоторой доли ностальгического шарма и утратил возможность наивной работы с графикой через прерывания BIOS. Однако переход на Windows вдохнул новую жизнь в проект и открыл дорогу к давней мечте — самокомпиляции.

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

sjph_5nwamax8td4bfxs_zmwuys.jpeg

Пять шагов к самокомпиляции в 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 элементов, но такие множества оказались бы практически бесполезны.

Обёртки для некоторых процедур. Идея самокомпиляции привела к обёртыванию вызовов некоторых процедур стандартной библиотеки. Сигнатура обёртки едина для случаев компиляции внешним компилятором (Delphi/Free Pascal) и самокомпиляции; обёртываемые процедуры при этом различаются. Таким образом, вся специфика способа компиляции локализуется внутри нескольких обёрток. Паскаль пестрит процедурами, которые при ближайшем рассмотрении оказывается невозможно реализовать по правилам самого Паскаля: Read, Write, Move и т.д. Для самых употребительных процедур, в том числе Read и Write, я сделал исключение и реализовал их нетипичными для грамматики языка, но привычными любому знатоку Паскаля. Для большинства остальных нетипичных процедур понадобились обёртки. Таким образом, XD Pascal не во всём совместим с Delphi или Free Pascal, однако большой беды в этом нет, поскольку даже сам Free Pascal в режиме совместимости с Delphi фактически остаётся несовместимым.

Компиляция программ с GUI


Задача самокомпиляции, несмотря на своё символическое значение, остаётся ограниченной: компилятор является консольной программой и поэтому выглядит не совсем полноправным обитателем мира Windows. Понадобилось ещё несколько нововведений на пути к компиляции программ с оконным интерфейсом:

Директива компилятору для установки типа интерфейса. Тип интерфейса (консольный или графический) должен быть указан в отдельном поле заголовка исполняемого файла. Как известно, в Delphi и Free Pascal для этого существует директива $APPTYPE. Аналогичная директива $A появилась и в XD Pascal.

Операция взятия адреса процедур и функций. В классическом Паскале нет полноценных указателей на процедуры и функции — их в некоторой мере заменяет процедурный тип. Этот тип в XD Pascal не реализован. Как бы то ни было, до сих пор применение операции @ к процедурам в моём скромном проекте казалось мне бесполезным. Однако обработка событий Windows API построена на обратных вызовах (callbacks), и здесь передача адреса вызываемой процедуры-обработчика вдруг стала насущной необходимостью.

Явное указание имён подключаемых библиотек. Для консольных программ было достаточно импорта функций Windows API из библиотеки KERNEL32.DLL. Программы с GUI потянули за собой USER32.DLL, GDI32.DLL и т.д. Понадобилось расширить синтаксис директивы external, добавив туда имя библиотеки.

bgilflzxiawknn-ij5x2b9xebko.png
Демонстрационная программа с GUI

Что в итоге


В результате получился очень простой самокомпилируемый компилятор для Windows. Вряд ли корректно сравнивать его с могучими коллективными проектами типа Free Pascal. Скорее он попадает в весовую категорию известного любительского BeRo Tiny Pascal. По сравнению с ним XD Pascal имеет заметные преимущества: более строго соблюдается грамматика Паскаля и контролируются ошибки, есть полноценный файловый ввод/вывод, поддерживается арифметика чисел с плавающей точкой, нет зависимости от внешнего ассемблера, допускается компиляция программ с оконным интерфейсом.

Далее мне предстоит разобраться с ложноположительным срабатыванием некоторых антивирусов — новой проблемой, о которой я и не задумывался в маленьком уютном мирке MS-DOS. Если повезёт, XD Pascal будет внедрён, наряду с BeRo Tiny Pascal, в лабораторный практикум по курсу конструирования компиляторов в МГТУ им. Н.Э. Баумана.

© Habrahabr.ru