Вставляем генератор кода в сборку qmake

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

    Зачем это нужно

    В текущем моем проекте возникла необходимость применить объектный подход при работе с данными, физически хранящимися в реляционной БД. Стало быть, ORM. Так уж вышло, что данный проект корпоративного сегмента разрабатывается на Qt. Да, хоть это и прекраснейший фреймворк, но не вполне подходящий под задачи программирования сложных корпоративных приложений. Тем не менее, выбор в пользу Qt был сделан по ряду весьма веских причин.
    Итак, есть небольшая реляционная БД из примерно 100 таблиц. Необходимо написать тонкий пока слой бизнес-логики, который в перспективе будет обрастать жирком. Имеется описание структуры БД в XML.
    Можно засучить рукава и за неделю написать 100 однотипных классов бизнес-логики. Написать многочисленные тесты, сравнивающие эти классы, XML-описание метаданных и саму структуру БД. Но, это не подход настоящего джедая! Действительно, ведь у нас уже есть все необходимое для описания прототипов классов бизнес-логики, просто нужно превратить .xml в .h.

    Генератор кода

    Для начала разберёмся с XML. Описание таблиц выглядит примерно так:
        <table name="TestTable" >
            <field name="id" type="INTEGER" />
            <field name="name" type="VARCHAR(100)" />
        </table>
    

    Можно конечно при помощи того же QDomDocument довольно быстро написать собственный препроцессор-генератор, но правильнее будет использовать XSL. Для XSLT-преобразований удобно использовать какой-нибудь консольный процессор. Я использовал xsltproc. Эта простая утилита берёт на вход XML, XSL и выводит получившийся текст куда скажут. Можно использовать другой инструмент, можно написать самому, это не принципиально. Используя нехитрый шаблон получаем
    class TestTable
    {
    public:
        int id();
        QString name();
    };
    

    Да, пока это тонкий-тонкий скелет, которому надлежит обрасти жирком еще на этапе автоматической генерации, но эти детали опустим. Нам пока важен сам принцип.
    Итак, мы уже умеем автоматически генерить код в любых количествах, осталось автоматизировать этот процесс.

    Автоматизация

    Я работаю в QtCreator и хочу включить свои XML-ки с описанием структуры данных прямо в проект. Для файлов, не участвующих в стандартном процессе сборки есть раздел проекта «Другие файлы». В .pro-файле он, что характерно, называется OTHER_FILES. Но не будем мешать мух с котлетами, заведём сразу свою секцию. Добавим туда наше описание, а потом добавим свою секцию к «Другим файлам»:
    ORM_FILES += classes.qoc
    OTHER_FILES += $$ORM_FILES
    

    Теперь начинается самое интересное. QMake — это невероятно навороченная штука. Одних ключевых слов описания pro-файлов там больше сотни. Одна из возможностей — QMAKE_EXTRA_COMPILERS. Это механизм указания в pro-файле правил запуска дополнительных компиляторов, препроцессоров и прочих интерпретаторов кода. Именно таким образом вызываются moc и uic. Оригинальное описание можно почерпнуть тут. Правда, оно довольно лаконичное. В нашем профайле должно быть дописано примерно следующее:
    orm.output = ${QMAKE_FILE_IN_PATH}/${QMAKE_FILE_IN_BASE}_qoc.h    #выходной файл
    orm.input = ORM_FILES         #список входных файлов
    orm.commands = xsltproc -o ${QMAKE_FILE_OUT} ${QMAKE_FILE_IN}    #команда 
    orm.variable_out = HEADERS    # куда добавить выходные файлы
    orm.name = ORM    # имя(для внутренних целей qmake)
    
    QMAKE_EXTRA_COMPILERS += orm
    

    Можно попробовать собрать, на данном этапе всё уже должно работать.

    Наводим порядок

    Вышеприведенный кусок на самом деле напоминает .prf-файлы конфигурируации qmake, которые располагаются в /usr/share/qt4/mkspecs/features если вы используете Линукс. Когда мы пишем
    CONFIG += xml 
    например, подключается конфиг xml.prf. Таким образом можно вынести эти инструкции в файл orm.prf и положить его в указанную папку. Тогда для обработки наших XML-ек достаточно будет написать
    CONFIG += orm
    


    Вместо заключения

    Теперь пару слов о возможных подводных камнях. Я практически сразу написал данный конфиг по мануалу, однако ничего не происходило. В консоли сборки не было запуска xsltproc и хедер не генерировался. Перепробовав разнобразнейшие варианты обнаружил, что xsltproc по крайней мере вызывается если написать orm.variable_out = SOURCES. Дальше обнаружил, что забыл в XSL-шаблоне написать #define'ы защиты от повторного включения хедера и после корректировки шаблона ВНЕЗАПНО конфиг заработал и стал исправно выдавать сгенерированный хедер. Я в чудеса не верю, списал это на четвертый час ночи. Но, если обнаружите нечто подобное — я вас предупредил.
    Share post

    Comments 16

      +1
      Когда-то подобным образом автоматически генерировал С++ интерфейсы из xml спецификаций D-Bus. С помощью утилиты qdbusxml2cpp.
        0
        Мы делаем ещё хитрее: у нас ряд функций ПО выносится в выполнение в отдельных процессах (a-la chromium). Сделано не столько из-за соображений безопасности, сколько из-за нестабильности ряда сторонних компонентов. Отдельный привет здесь передаю реализации Mico ORB :)

        В удалённых модулях пишутся классы для экспорта через D-Bus, из них генерируются XML-файлы с описанием с помощью утилиты qdbuscpp2xml, для вызывающей стороны из этих XML генерируются классы интерфейсов через qdbuscpp2xml.

        Главная проблема, на которую сейчас напоролись — QtDBus ужасно тормозит на передаче больших (порядка 1-2 Мб) блобов через шину. На девелоперской машине (Core i7-2600K) на блоб весом в 2 Мб уходит порядка секунды :(
        –4
        Чем создание классов скриптом отличается от ручного копипастинга? Такой же быдлокод по сути. Кодогенерация оправдана только там, где результата нельзя добиться нормальным способом, или где она дает прирост производительности. Не говоря о том, что для 100 сгенерированных классов каждый метод будет сгенерирован в 100 экземплярах (что здорово раздует размер бинарника). Надо применять нормальный ООП-подход или наследование. не все, правда, это умеют.
          +3
          >>Чем создание классов скриптом отличается от ручного копипастинга?
          Скоростью. Меньшей ценой ошибки (всегда можно перегенерить всё)

          >>Кодогенерация оправдана только там, где результата нельзя добиться нормальным способом, или где она дает прирост производительности
          Извините, это полная ерунда. Нет такого кодогенератора который напишет лучше чем программист руками. Никакой кодогенератор не сгенерить более производительный код чем опытный программист

          >>Надо применять нормальный ООП-подход или наследование.
          Кодогенерация не отрицает ООП и наследование, даже наоборот.

          Кодогенерация оправдана там, где нужно писать много рутинного бессодержательного кода. При работе с базами данных это действительно актуально.
          Не верите мне — почитайте Фаулера, например, «Шаблоны корпоративных приложений»

          +1
          Я бы на вашем место хорошенько подумал, стоит ли заниматься кодогенерацией, или лучше писать код, который будет выполнять действия согласно структуре, описанной в XML.

          Об этом лучше подумать сейчас, потом будет поздно.
            0
            Да, я прекрасно осознаю что это решение, которое потом невозможно исправить. Поэтому я придумал снизить риск путем выноса бизнес-логики в плагины. Сам движок приложения будет лишён бизнес-логики, будет работать по правилам описанным в XML. Однако для описания поведения декларативный XML не очень годится. Нужен полноценный язык. Возможно, еще изменю решение в пользу QScript, он показывает неплохую производительность кстати. Либо бинарный плагин со скриптом внутри.
            +1
            Кодогенерация — это априори грабли при портировании, особенно если для неё нужна специальная утилита, которой может не быть на целевой платформе. При кросскомпиляции так вообще кодогенератор все может колом накрыть если кодогенерацией занимается не утилита конфигурирования и сборки.
              0
              Никто особых проблем с тем же moc и uic не испытывает. Кодогенератор можно на самом Qt написать для лучшей портируемости
                0
                Там хитрости идут некоторые, но вообще иметь секас со сборкой проги по одной спеке а утилит по другой обеспечен. Хотя qbs тут в принципе готов зверски помочь, ибо можно будет кодогенератор просто плагином для qbs собрать и подгрузить.
                  0
                  Не вижу особой разницы — подгрузить плагин или выполнить консольную команду. С командой даже проще в том плане что можно взять готовую, а плагин еще написать придется.

                  Впрочем QBS штука интересная, заслуживает. Но пока она экспериментальная — пользуемся qmake'ом.
                    0
                    На CMake делается без проблем. Можно делать так, что исходные файлы для одной из целей проекта будут генерироваться утилитой, собираемой вместе с проектом в рамках другой цели. Он сам разрулит зависимости и установит правильный порядок сборки.

                    Проверено на людях :)
                      0
                      Не в этом сложность. Тебе нужно собирать софт для винды, в процессе сборки у тебя генерится утилита, в случае кросскомпиляции нужно заставить её для платформы хоста скомпилить, что малость непонятно как сделать. Форсить платформу? Но как?
                        0
                        Реализуемо, хоть и затруднительно. Как такое на qmake сделать — не знаю, но я с ним вообще довольно поверхностно знаком, необходимости не было :)

                        У нас в принципе не стояло задачи сделать поддержку кросс-компиляции. Мы же здесь ведём речь о бизнес-приложениях, для них такая цель вообще ставится редко (у нас платформа сборки вообще по большому счёту одна).
                +1
                А я ожидал генерации исходников средствами лишь qmake'а… Да, сторонние утилиты портят картину.
                Но всё равно — спасибо за статью!
                  0
                  Вы меня натолкнули на мысль собирать кодогенератор во время сборки целевого проекта :) Но это уже достойно блога «Ненормальное программирование»
                  0
                  Для заинтересовавшихся, вот пост в моем блоге с подробным описанием того, что умеет QMAKE_EXTRA_COMPILERS.

                  Only users with full accounts can post comments. Log in, please.