Самодельный компилятор и игровая библиотека Raylib. Опыт стыковки
Говорят, что успех того или иного языка программирования или компилятора во многом зависит от его умения взаимодействовать со сторонним кодом. Конечно, «успех» любительского компилятора нужно понимать с известной долей условности и даже иронии. Однако и здесь интеграция с внешними библиотеками, написанными на С, может стать неплохой школой жизни.
О моём компиляторе XD Pascal уже было несколько постов на Хабре. Компилятор написан предельно просто и целиком вручную, при этом язык имеет весьма нетипичные расширения — методы и интерфейсы, позаимствованные из Go. На сегодняшний день базовый язык реализован полностью, работает самокомпиляция, введены простейшие оптимизации. Тут и возникло естественное желание наладить взаимодействие компилятора с какой-нибудь несложной игровой библиотекой. Выбор пал на Raylib —, но никогда бы он на неё не пал, если бы я сразу предвидел её подводные камни. Невинная затея превратилась в борьбу с соглашениями о вызове.
Дьявол в деталях
Библиотека Raylib приглянулась мне тем, что она относительно невелика, активно развивается, почти не содержит внешних зависимостей и имеет готовую обвязку для Паскаля. Кроме того, вся вещественная арифметика в ней — одинарной точности. К сожалению, это актуально для меня, ибо двойной точности в моём XD Pascal пока так и не появилось.
Сначала казалось, что от меня потребуется немного — всего-то реализовать соглашение о вызове cdecl
, принятое в Raylib. Поддержка stdcall
у меня уже была, поскольку приходилось взаимодействовать с Windows API. Оставалось лишь научиться очищать стек вызова не внутри функции, а на вызывающей стороне.
Далее обнаружилось странное. Какая-то дьявольская сила заставила автора Raylib использовать в своём API передачу структур в функции по значению и возвращение структур как результата. Не раз говорилось о том, что это плохая практика, и к чести разработчиков Windows API надо сказать, что они этого всячески избегали. Но только не разработчик Raylib. Отсюда родилось немало проблем — и объективных, и субъективных.
Передача структур по значению
Эта проблема скорее субъективная, хотя и не во всём. Дело в том, что мой XD Pascal был с самого начала спроектирован под генерацию кода на лету, без явного построения абстрактного синтаксического дерева (AST). В своё оправдание могу сказать лишь то, что такими же были все ранние компиляторы Паскаля, включая незабвенный Turbo Pascal, да и сам язык конструировался Никлаусом Виртом именно под компиляцию без AST.
Этот подход был вполне приемлем до возникновения потребности взаимодействовать с кодом на С. Компилятор без AST может помещать фактические параметры функции в стек ровно в том порядке, в каком они перечислены в исходном тексте — слева направо. Однако код на C со своими соглашениями cdecl
и stdcall
ожидает обратного порядка. Не составляет особой проблемы «перевернуть» стек, если заранее известно, что все параметры имеют строго одинаковый размер (например, 4 байта), как это имеет место в Windows API. Но если в стеке появляются структуры произвольного размера, «переворот» стека становится намного сложнее. Сейчас приходится с ним мириться; может быть, переход на AST когда-то избавит меня от этой несуразности.
Конечно, в проблеме передачи структур есть и объективная сторона, связанная, например, с выравниванием. И в Raylib, и в XD Pascal все поля структур не имеют выравнивания, а структуры как целое выравниваются по 4 байтам. Здесь для меня никаких сложностей интеграции не возникло, однако я не рискну утверждать, что такое соглашение переносимо на другие компиляторы и платформы.
Возвращение структуры как результата
Структура как результат функции — это уже серьёзная и абсолютно объективная проблема. Остаётся только удивляться, как индустрия IT допустила столь вопиющий хаос, скрывающийся за директивами cdecl
и stdcall
. Общей здесь является только идея выделять в стеке вызывающей стороны место под результат функции, а затем передавать в функцию скрытый параметр-указатель на выделенное место. Но дальше возникают вопросы, на которые каждый отвечает по-своему. В какой позиции должен быть скрытый параметр? Нужно ли от него отказываться, если структура-результат целиком умещается в регистре? А в двух регистрах?
Microsoft попыталась навести у себя порядок, постановив:
On x86 plaftorms, all arguments are widened to 32 bits when they are passed. Return values are also widened to 32 bits and returned in the EAX register, except for 8-byte structures, which are returned in the EDX: EAX register pair. Larger structures are returned in the EAX register as pointers to hidden return structures.
Эта несколько туманная формулировка оставляет неясной судьбу структур, например, длиной 7 байт. При этом автор одного мучительно обстоятельного исследования утверждает, что реальное поведение компилятора Visual C++ при отсутствии выравнивания структур вообще не соответствует документации.
С промышленными компиляторами Паскаля дело обстоит ещё хуже. Free Pascal (в режиме Delphi) оказывается несовместим с Delphi 6 даже при передаче 8-байтных структур с соглашением cdecl
. Free Pascal старается следовать предписанию Microsoft — в данном случае оно вполне однозначно. Тем временем Delphi 6 создаёт скрытый параметр-указатель и не возвращает ничего полезного в регистрах EDX: EAX. Я следовал примеру Free Pascal, поскольку именно этот вариант реализуется в Raylib. Подозреваю, что работать с Raylib из Delphi 6 вообще невозможно. Не знаю, изменилось ли что-то в новых версиях Delphi.
Итог
В качестве компромисса в XD Pascal реализована следующая логика использования cdecl
и stdcall
: структуры не более 4 байт возвращаются по значению в EAX, структуры не более 8 байт — в EDX: EAX, все прочие — через скрытый параметр-указатель, передаваемый последним. К счастью, в Raylib нет структур по 3 или 7 байт, так что связанную с ними неясность можно пока обойти стороной.
Библиотека Raylib в целом успешно состыковалась с моим самодельным компилятором. Ложкой дёгтя осталась единственная функция GetTime
, возвращающая Double
.