В августе 2019 года в CMake появилась долгожданная поддержка предкомпилированных заголовков. До этого приходилось использовать разные плагины, например, Cotire. Сразу после выпуска CMake с новым функционалом было ещё несколько доработок. Но уже осенью мы посчитали, что можно начать использовать эту фичу, и переписали наши скрипты. Позже мы нашли баг, который формировал неправильные параметры компилятора Clang и мешал запуску анализатора PVS-Studio. Баг пришлось исправить самим.
Проблема была в том, что исходный файл, в котором использовался предкомпилированный заголовок, был некорректно препроцессирован, если использовался компилятор Clang. На месте header-файла было пустое место. Но в GCC и MSVC все работало нормально.
Стоит рассказать о том, как CMake формирует предкомпилированный заголовок. Для каждого target'а (а начиная с CMake 3.17 и для каждого типа сборки – Debug, Release, ...) создается 2 файла: cmake_pch.hxx и cmake_pch.cxx. hxx-файл – это header-файл, из которого уже будет сформирован предкомпилированный заголовок, а cxx – пустой файл с содержимым:
/* Generated by CMake */
Нужен он для генерации предкомпилированного заголовка – он используется как исходник при запуске компилятора.
Во время сборки проекта заголовочный файл включается через передачу флагов компилятора. Для Clang это:
-Xclang -include-pch -Xclang <PCH_FILE>
Т.е. в команду компиляции передается уже созданный предкомпилированный заголовок. В итоге команды запуска сохраняются в файле compile_commands.json, который анализатор использует для проверки проекта.
Однако на этом моменте имя оригинального header-файла потеряно. При запуске препроцессора Clang пытается достать имя заголовочного файла из созданного предкомпилированного заголовка и в итоге использует пустой файл cmake_pch.cxx. Почему? Если взглянуть в документацию Clang, то можно увидеть их способ создания предкомпилированных заголовков:
To generate PCH files using clang -cc1, use the option -emit-pch:
$ clang -cc1 test.h -emit-pch -o test.h.pch
Заголовочный файл test.h используется в качестве исходного. А вот как делает CMake:
-Xclang -emit-pch -Xclang -include -Xclang <PCH_HEADER>
Он включает его через специальный флаг, а затем передает cmake_pch.cxx как исходник. Информация о включенных файлах, переданных через командную строку, к сожалению, теряется, а информация об исходном файле остается. Это можно узнать, посмотрев документацию Clang:
Original file name
The full path of the header that was used to generate the AST file.
Cotire по своей структуре похож на CMake, однако cxx-файл выглядел примерно так:
#ifdef __cplusplus
#include "/path/to/header.hxx"
#endif
В исходнике у него была директива include, поэтому препроцессор работал нормально. Это первое, что мы хотели предложить в качестве исправления.
Однако при таком подходе появились проблемы - нарушалась кодировка файлов (компилятор может преобразовывать исходный файл в свою кодировку), могла сломаться работа других компиляторов, а в CMake 3.17 файл cmake_pch.cxx создавался только один для каждого типа сборки, когда hxx-файлы могли отличаться.
В итоге было решено передавать в строку компиляции еще и оригинальный исходный файл:
-Xclang -include-pch -Xclang <PCH_FILE> -Xclang -include -Xclang <PCH_HEADER>
Благодаря опции -Xclang следующий флаг передается напрямую в препроцессор, минуя драйвер, который выполняет поиск соответствующего заголовку .pch/.gch файла. Ссылка на патч: PCH: Clang: Update PCH usage flags to include original header.
Наш небольшой патч вошел в CMake 3.17, исправив проблему. Надеюсь, было интересно, и вы узнали чуть больше о работе CMake.
Мы можем рекомендовать использовать эту фичу в реальных проектах.
0