Я пробовал много «новых» языков программирования, но как-то так получается, что всегда, когда я возвращаюсь к С++ или даже к C, это наполняет меня радостью. Правда, когда я возвращаюсь к этим языкам, кое-что меня слегка раздражает. Это — то, что мне нужны заголовочные файлы с объявлениями, а так же — отдельный файл, в котором продублирована почти та же самая информация. Я постоянно вношу в код изменения и забываю обновлять заголовочные файлы. Эти файлы не генерируются автоматически при использовании C++ и C. А инструменты, сопутствующие другим языкам, сами заботятся о подобных вещах. Это привело меня к поискам чего-либо, позволяющего автоматизировать генерирование заголовочных файлов. Конечно, некоторые IDE автоматически вставляют объявления туда, куда нужно, но меня всё это, по многим причинам, никогда полностью не устраивало. Мне хотелось что-то маленькое и быстрое, такое, что я мог бы использовать в составе множества различных наборов инструментов.



Я нашёл один довольно старый инструмент, который хорошо справляется с этой задачей. У него, правда, имеются определённые ограничения. Инструмент этот кажется немного запутанным. Поэтому я решил, что о нём стоит рассказать. Речь идёт о makeheaders. Это — часть системы управления конфигурацией программ Fossil. История программы восходит к 1993 году, когда Дуэйн Ричард Хипп (тот самый программист, который создал SQLite) написал её для собственных нужд. Эта программа не особенно сложна: вся она помещается в довольно большом C-файле. Но своё дело она делает: сканирует директорию и создаёт для всего, что найдёт, заголовочные файлы. В некоторых случаях для применения makeheaders нет нужды делать больших изменений в исходном коде, но, если есть такая необходимость, кое-что в нём можно и изменить.

Все переводы серии
Кунг-фу стиля Linux: удобная работа с файлами по SSH
Кунг-фу стиля Linux: мониторинг дисковой подсистемы
Кунг-фу стиля Linux: глобальный поиск и замена строк с помощью ripgrep
Кунг-фу стиля Linux: упрощение работы с awk
Кунг-фу стиля Linux: наблюдение за файловой системой
Кунг-фу стиля Linux: наблюдение за файлами
Кунг-фу стиля Linux: удобный доступ к справке при работе с bash
Кунг-фу стиля Linux: великая сила make
Кунг-фу стиля Linux: устранение неполадок в работе incron
Кунг-фу стиля Linux: расшаривание терминала в браузере
Кунг-фу стиля Linux: синхронизация настроек
Кунг-фу стиля Linux: бесплатный VPN по SSH
Кунг-фу стиля Linux: превращение веб-приложений в полноценные программы
Кунг-фу стиля Linux: утилита marker и меню для командной строки
Кунг-фу стиля Linux: sudo и поворот двух ключей
Кунг-фу стиля Linux: программное управление окнами
Кунг-фу стиля Linux: организация работы программ после выхода из системы
Кунг-фу стиля Linux: регулярные выражения
Кунг-фу стиля Linux: запуск команд
Кунг-фу стиля Linux: разбираемся с последовательными портами
Кунг-фу стиля Linux: базы данных — это файловые системы нового уровня
Кунг-фу стиля Linux: о повторении кое-каких событий сетевой истории
Кунг-фу стиля Linux: PDF для пингвинов
Кунг-фу стиля Linux: делаем все и сразу
Кунг-фу стиля Linux: файловые системы пользовательского пространства теперь доступны и в Windows
Кунг-фу стиля Linux: делиться — это плохо
Кунг-фу стиля Linux: автоматическое генерирование заголовочных файлов

Обзор проблемы


Предположим, имеются C-файлы, работающие совместно. Пусть это — файлы A.c и B.c. В файле A.c находится простая функция:

double ctof(double c)
{
  return (9.0*c)/5+32.0;
}

Если предполагается использовать эту функцию в файле B.c, там должно быть объявление этой функции, чтобы, когда компилируется B.c, компилятор мог бы узнать о том, что функция принимает единственный аргумент типа double и возвращает значение того же типа. В коде, написанном на ANSI C (и на C++) понадобится примерно такая конструкция:

double ctof(double c);

Это — не исполняемый код. Это — всего лишь подсказка компилятору о том, как выглядит функция. Такие конструкции называют прототипами функций. Обычно создают заголовочный файл, содержащий подобный прототип. Включить этот файл можно и в A.c, и в B.c.

Проблема возникает тогда, когда меняют функцию в A.c:

double ctof(double c1, double c2)
{
  return (9.0*(c1+c2))/5+32.0;
}

Если не привести содержимое заголовочного файла в соответствие с новым описанием функции — жди неприятностей. Причём, объявление функции в заголовочном файле должно точно соответствовать её определению в файле исходного кода. Если, например, ошибиться и указать в заголовочном файле, что аргументы функции имеют тип float — то, что получится, работать не будет.

Программа makeheaders


Если у вас имеется makeheaders — вы можете просто запустить эту программу, передав ей все C- и H-файлы, которые ей нужно просканировать. Обычно для этого достаточно воспользоваться glob-шаблоном *.[ch]. Программа может обрабатывать и CPP-файлы, и даже — наборы файлов разных типов. Она, по умолчанию, помещает все объявления глобальных переменных и определения глобальных функций в набор заголовочных файлов.

Почему речь идёт о «наборе заголовочных файлов»? Дело в том, что работа программы основана на одном предположении, которое, если о нём не задуматься, может показаться странным. Так как заголовочные файлы генерируются автоматически — нет смысла повторно их использовать. В результате программа помещает в них то, что нужно, располагая это в правильном порядке. Так, файл A.c будет использовать заголовочный файл A.h, а B.cB.h. Между этими двумя файлами не будет перекрёстных зависимостей. Если что-то изменяется, makeheaders просто запускают снова и она создаёт нужные заголовочные файлы.

Что попадает в заголовочные файлы?


Вот что документация к makeheaders говорит о том, что программа копирует в заголовочные файлы:

  • Если функция определена в одном из C-файлов, прототип этой функции помещается в сгенерированный H-файл, предназначенный для любого из C-файлов, вызывающих эту функцию. Если в определении функции используется ключевое слово static — прототип в H-файле не размещается. Если там, где обычно пользуются static, используют ключевое слово LOCAL, тогда прототип генерируется, но размещается он лишь в единственном заголовочном файле, соответствующем тому файлу исходного кода, который содержит такую функцию. А в H-файлы, предназначенные для других C-файлов, прототип LOCAL-функции не попадает, так как область видимости такой функции ограничена тем файлом, в котором она определена. Если вызвать makeheaders с опцией командной строки -local — тогда программа будет воспринимать ключевое слово static как LOCAL и создаст прототипы соответствующих функций в заголовочном файле, предназначенном для файла исходного кода, содержащего определения таких функций.
  • Если в C-файле определена глобальная переменная, её объявление с ключевым словом extern помещают в заголовочные файлы, предназначенные для тех C-файлов, которые используют эту переменную. Если в H-файле, который создан вручную, встречается объявление структуры, объединения, перечисления, или прототип функции, или объявление C++-класса, всё это копируется в H-файлы, сгенерированные автоматически для всех функций, использующих то, что описано в H-файле, созданном вручную. Но объявления, встречающиеся в C-файлах, считаются внутренними, предназначенными для конкретных файлов, они не копируются в файлы, генерируемые автоматически.
  • Все конструкции #define и typedef, которые встречаются в H-файлах, созданных вручную, копируются, по необходимости, в H-файлы, созданные автоматически. Такие же конструкции, имеющиеся в C-файлах, считаются внутренними и не копируются. Если объявление структуры, объединения или перечисления расположено в H-файле — makeheaders автоматически сгенерирует конструкцию typedef, которая позволит ссылаться на объявление без использования квалификаторов struct, union или enum.

Обратите внимание на то, что makeheaders распознаёт файлы, созданные этой программой. Поэтому нет нужды исключать их из состава входных файлов, передаваемых ей при её вызове.

Пример на C++


В случае с чем-то вроде C++-классов, или, на самом деле, в случае с чем угодно, можно поместить блок кода в специальные директивы препроцессора для того чтобы программа makeheaders обработала бы этот блок. Вот — простой пример кода, который я использовал для того чтобы это проверить.



Тут стоит обратить внимание на несколько моментов:

  • Команда включения в C++-файл файла test.hpp возьмёт на себя задачу по включению в С++-файл и сгенерированного специально для него заголовочного файла.
  • Директива INTERFACE заключает в себя код, который должен попасть в заголовочный файл. Во время компиляции INTERFACE будет равно нулю, поэтому код не будет компилироваться дважды.
  • Функции-члены объявлены за пределами раздела INTERFACE с использованием ключевого слова PUBLIC (там, конечно, могут использоваться и ключевые слова PRIVATE или PROTECTED). Это приведёт к тому, что makeheaders обратит внимание и на них.

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

Обратите внимание на то, что, при использовании PUBLIC или других ключевых слов, функции изымают из объявления. Единственная причина, по которой в примере имеются некоторые функции, заключается в том, что они являются встроенными. Если поместить все функции за пределами блока INTERFACE, сгенерированный заголовочный файл правильно составит объявление класса. В данном случае он добавит эти функции к тем, которые уже имеются.

Сгенерированный заголовочный файл


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

Вот этот файл:



Обратите внимание на то, что INTERFACE здесь, в самом конце, устанавливается в 0. А это значит, что в файле с исходным кодом раздел INTERFACE не будет подвергаться повторной компиляции. В случае с C-файлами makeheaders, кроме того, генерирует конструкции typedef для чего-то вроде структур. В C++ это, конечно, не нужно. Тут можно видеть побочный эффект наличия некоторых объявлений в разделе INTERFACE, а некоторых — в разделе реализации: тут имеется избыточный тег PUBLIC. Вреда от этого нет, этот тег не появился бы в том случае, если бы я поместил весь код за пределами раздела INTERFACE.

Это ещё не всё


Гибкий инструмент, который мы рассмотрели, умеет и кое-что ещё. Узнать об этом можно из документации по нему. Он поддерживает флаг, благодаря которому выдаётся информация об обрабатываемом коде, которую можно использовать для его документирования. Можно создавать иерархии интерфейсов. Makeheaders, кроме того, может помочь в работе над проектами, где совместно используется C++ и C. Этот инструмент достаточно интеллектуален и поддерживает условную компиляцию. Правда, стоит учитывать то, что в число поддерживаемых им механизмов C++ не входят шаблоны и пространства имён. Правда, код makeheaders открыт, поэтому вы, если хотите, можете этот инструмент доработать. Прежде чем вы решитесь на применение makeheaders в крупном проекте — учтите, что у этого инструмента есть и ещё некоторые ограничения.

Планируете ли вы попробовать нечто вроде makeheaders, или вас устраивает ручная работа с заголовочными файлами?

Комментарии (8)


  1. light_and_ray
    18.01.2022 16:39
    -2

    Вы не добавили хабы C, C++


  1. oleg-m1973
    18.01.2022 19:44
    -3

    Все эти "генераторы кода", которые я видел (включая midl), как правило очень плохие, т.е. генерят какой-то отвратительный код. Они совершенно не предназначены для современных компиляторов С++.
    Они просто генерят тупую реализацию, безо всякой возможности расширения и обобщённого использования. Это бы проканало для какого-нибуть сраного си-шарпа, но не в с++.


  1. fk0
    19.01.2022 13:35

    Я вообще не понимаю, как можно "сгенерировать" заголовочный файл, т.к. обычно он является сущностью используемой другими .c/.cpp файлами, а не наоборот. И в нём определяются интерфейсы и типы какие должны быть, а не какие получились по факту. Нормально программирование "сверху-вниз" начинается с .h файла.


    1. mayorovp
      19.01.2022 15:11

      Нормальное программирование — это то как сделано почти во всех языках кроме Си, С++, ассемблера и Visual Prolog (список не полный): все сущности из модуля доступны другим модулям либо по умолчанию, либо после импорта модуля (именно самого модуля, а не отдельного заголовочного файла для него).


      Здесь просто пытаются сделать в Си/С++ так же нормально, как и в других языках.


  1. ruomserg
    19.01.2022 17:03

    Когда-то давно (еще в институте) — делал скрипт c2h, который ловил внутри c/cpp файлов конструкции вида:

    //EMBED_HEADER: foo.h
    #ifndef FOO_H
    #define FOO_H
    class foo {
     public:
      inline void bar() {return;}
      void baz(void);
    };
    #endif
    //HEADER_EOF
    void
    foo::baz(void) { return; }
    


    И дальше в Makefile указывалось, как получить .h файл из .cpp (через generic rules), а также использовалась черная магия make depend, которая позволяла автоматически вычислить — какие .c файлы от каких .h зависят (через запуск GCC с каким-то флагом и включением результата его работы в Makefile).

    Помню еще, что для ускорения компиляции скрипт c2h в какой-то момент был усовершенствован чтобы добавлять в комментарии ".h" файла хэш-сумму ".c"/".cpp" файла из которого он сделан, и если исходный файл не менялся от компиляции до компиляции — то файл-хедер не перегенерировался. Во времена, когда вместо SSD-дисков и ксеонов были HDD мегабайт на 40 и пентиумы-MMX, отказ от перезаписи кучи мелких файлов ускорял перекомпиляцию раза в два…

    К слову сказать, в редакторе Multi-Edit (кто его помнит по DOS) были три чудесных вида блоков: блок строк, блок колонок, и блок потоком. Я до сих пор помню чудо, которое совершал блок первого типа: нажимаешь на кнопку (кажется, F7), и вне зависимости от позиции в которой стоит твой курсор — вся строка выделяется в блок. Потом клавишами вверх-вниз меняешь его размер (целыми строками, прикиньте!), и еще раз F7 — и блок выделен. И не сбрасывается если тыкать в экран мышкой или ходить клавишами: персистентный он. А в другом окне можно было нажать «Insert from other file», и тебе показывали список всех окон где имеются выделенные блоки — и ты из них выбирал нужное. И это было намного удобнее чем гребаный буфер обмена и «блок-потоком» исчезающий при любом взаимодействии с текстом — как это принято в всех современных IDE… Доживу до пенсии — буду прикручивать это к Eclipse, наверное… :-)


    1. shovdmi
      19.01.2022 19:10

      в Windows 10 есть сочетание клавиш Win+v (как замена Ctrl+v), которое дает выбрать из истории того, что было скопировано в буфер по Ctrl+c.


  1. Overphase
    20.01.2022 10:59

    В отличие от большинства статей цикла эту можно отнести к вредным советам. Если у вас возникла необходимость изменить определение функции, то приведение объявления в заголовочном файле - меньшая из возникающих проблем. Надо во всем проекте править вызовы функции или ссылки на неё. Если необходимость исправить определение функции возникла из-за того, что в каком-то месте не получается вызвать её такой, какая она есть сейчас, это свидетельствует о том, что интерфейс функции не продуман; и опять же, исправление объявления в заголовке - это меньшая по сути проблема.

    Как то ни было, я нахожу разнесение объявления и определения полезным инструментом, позволяющим программисту воздерживаться от слишком поспешного исправления API. Не стоит лезть в программирование админскими методами.


    1. ruomserg
      20.01.2022 15:22

      Ну нет! В C/C++ различные объявления одного и того же объекта в разных файлах могут привести к трудноуловимым ошибкам. Потому что одна часть программы будет считать что объект имеет один размер и в нем такие-то смещения. А другая часть — будет считать по-другому. И они будут портить объект в памяти друг для друга! Поэтому держать .h и .cpp в одном физическом файле — хорошо и правильно. А вот собирать .h из содержимого cpp — вряд ли. По-моему, последний кто это систематически делал — это arduino. Работало, но только для совсем мелких проектов…