Говорят, что успех того или иного языка программирования или компилятора во многом зависит от его умения взаимодействовать со сторонним кодом. Конечно, «успех» любительского компилятора нужно понимать с известной долей условности и даже иронии. Однако и здесь интеграция с внешними библиотеками, написанными на С, может стать неплохой школой жизни.
О моём компиляторе 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
.
Автор: Василий Терешков