Самодельный компилятор и игровая библиотека Raylib. Опыт стыковки

    image

    Говорят, что успех того или иного языка программирования или компилятора во многом зависит от его умения взаимодействовать со сторонним кодом. Конечно, «успех» любительского компилятора нужно понимать с известной долей условности и даже иронии. Однако и здесь интеграция с внешними библиотеками, написанными на С, может стать неплохой школой жизни.

    О моём компиляторе 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. (Дополнение: функция теперь поддерживается).
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 15

      0
      Нагуглилась книжка, содержащая сводную информацию по передаче параметров в разных компиляторах.
        0
        О, спасибо! У этого Агнера Фога я встречал отличные материалы по длительности выполнения машинных инструкций на x86.
          0
          Любопытно, что и в этой книжке есть ошибки. Например, в таблице 7 указано, что MSVC возвращает вещественный результат в целочисленных регистрах. А он это делает через регистр FPU.
          0
          Но если в стеке появляются структуры произвольного размера, «переворот» стека становится намного сложнее.

          А разве большие структуры не передаются как указатель на стеке? Не уверен насчет 32 битов, но на 64 битах в Windows структуры передаются именно так.
            0
            Нет. При 32 битах структуры, передаваемые по значению, помещаются в стек целиком. В книжке, на которую ссылается Siemargl, это указано в таблице 6.
            0
            Подозреваю, что работать с Raylib из Delphi 6 вообще невозможно.


            Обычно в случае подобных проблем на Дельфи можно было выкрутиться при помощи ассемблерных вставок.
              0
              Можно, но это всегда некий намёк на ограниченность того высокоуровневого языка, который мы выбрали. Хотелось бы оставаться в рамках этого языка и иметь под рукой всё, что нужно.
                0
                Ну, практически все языки высокого уровня имеют свои ограничения, особенно в таких тонких вопросах, как недостаточно стандартизованные ABI. Если бы Raylib использовала более стандартный stdcall, как это делают, кажется, все DLL самой Windows, проблем бы не было.
                  0
                  Не помогло бы. stdcall отличается от cdecl только ответственностью за очистку стека. Оба соглашения вполне однозначны, пока речь идёт о простейших типах и указателях, и оба погружаются в одинаковый хаос, когда дело касается передачи и возвращения структур.
                    0
                    (Освежив память) Да, вы правы. Проблем с вызовом системных DLL нет не столько из-за stdcall как такового, сколько из-за того, что они практически не передают структуры через стек, только указатели.
                    Но, опять же, использование ассемблерных вставок для оборачивания таких нестандартизованных вызовов мне представляется приемлемым решением. Хотелось бы обойтись без таких костылей, но что делать.
              –1
              Занимаясь чем то «похожим» пришёл к выводу, что бессмысленно пытаться оживить «Франкенштейна». Вот зачем все эти костыли и усилия, трата времени. Надо сделать заново всё, и как положено. Без оглядки на всякие там Си. Прошу прощения у автора за советы. Статья интересная, почитал с удовольствием.
                0
                Не совсем понял, что именно вы предлагаете сделать «без оглядки на всякие там Си». Сейчас любой новый язык программирования или компилятор должен уметь взаимодействовать с C, поскольку на C написаны миллиарды строк кода. Кто этого не сумеет — тот точно останется за бортом. Посмотрите, сколько усилий для налаживания взаимодействия приложили, например, разработчики Питона или Go.
                Другое дело, что лично мне следовало бы сделать AST и впредь не «переворачивать» стек. Но это лишь одна частная проблема. Все остальные останутся как есть.
                  0
                  Я согласен с вашими аргументами. Вопрос в другом, устраивает ли вас качество современного программного обеспечения? Меня категорически не устраивает. В пределе все эти миллионы строк ничего не стоят. Постоянно в новостях «обнаружена уязвимость ...». Программы глючат и падают. Если вас это устраивает, тогда вопросов нет.
                    0
                    Довольно наивно думать что, написав все с нуля, будет меньше ошибок, чем в уже оттестированном годами коде.

                    А так меня тоже не устраивает, см.мои публикации =)
                      0
                      А, вы предлагаете создать с нуля всю индустрию IT! Благое пожелание. Но если вы пересчитаете эти миллиарды (не миллионы) строк кода в человеко-годы труда, вы, наверное, испугаетесь. Я думаю, выйдет задача лет на 50-60 — то есть именно столько, сколько на самом деле и развивается программирование. А главное, уязвимости всё равно останутся: во-первых, люди по-прежнему несовершенны и допускают ошибки, а во-вторых, есть среди них и те, кто намеренно весь свой талант бросает на поиск и эксплуатацию уязвимостей.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое