Ад системных заголовков: Как мы учили libclang понимать GCC и выживать с MSVC

 · 3 min read

После того как мы перевели парсер C++ на libclang, мы ожидали увидеть идеально точное абстрактное синтаксическое дерево (AST). Но когда мы натравили его на первый же реальный проект, вместо красивой иерархии классов мы получили дерево, состоящее из сплошных CXCursor_UnexposedDecl и фатальных синтаксических ошибок.

Libclang System Headers Gcc Msvc

Парсер спотыкался на первой же строчке: #include <vector>.

Глухой компилятор

Оказалось, что libclang (будучи просто подключаемой библиотекой, а не полноценным CLI-компилятором) по умолчанию понятия не имеет, где в операционной системе лежат стандартные заголовочные файлы.

Если парсер не видит системных заголовков, он не понимает, что такое std::string или std::vector. А поскольку в современном C++ всё завязано на шаблонах стандартной библиотеки, парсинг ломается каскадом.

В Enterprise-мире разработчики обычно сами передают парсерам гигантские простыни из флагов -I/usr/include/.... Но мы строили SaaS и удобную CLI-утилиту. Мы не могли заставлять пользователей вручную искать пути к компиляторам на своих машинах. Парсер должен был стать умным.

Квест по поиску GCC

Libclang System Headers Gcc Msvc Part 2

На Linux-системах нам пришлось написать собственный механизм автообнаружения (Auto-Discovery).

Мы реализовали эвристику, которая в фоне вызывала системный компилятор с хитрыми флагами: gcc -print-file-name=include (с фоллбэком на cc). Этот трюк заставляет GCC (или системный Clang) выплюнуть в консоль список всех директорий, которые он использует для поиска системных заголовков по умолчанию.

Наш Python-код перехватывал этот вывод, парсил его, фильтровал несуществующие директории и динамически конвертировал в массив флагов -I, которые затем скармливались в libclang. Парсер “прозрел” — он начал идеально понимать Linux-проекты.

Кошмар MSVC-STL на Windows

Но если на Linux всё решилось элегантным хаком, то на Windows нас ждал настоящий ад.

Разработчики под Windows используют компилятор Microsoft Visual C++ (MSVC) и его проприетарную реализацию стандартной библиотеки — MSVC-STL. Интеграция Clang с хитросплетениями виндового кода (включая монструозный windows.h) — это боль.

MSVC исторически прощает многие вольности синтаксиса и использует специфичные макросы, прагмы и расширения. Строгий libclang смотрел на код из MSVC-STL и просто отказывался его парсить, генерируя сотни ошибок.

Чтобы помирить их, нам пришлось глубоко закопаться в документацию LLVM. Мы подобрали специфичный набор флагов совместимости (-fms-compatibility, -fms-extensions, а также подменять макросы -U__clang_major__ -D__clang_major__=19 для обхода проверок версии в MSVC STL), которые заставлялиlibclang` притворяться компилятором от Microsoft. Только после тонкой настройки виртуального окружения парсер перестал паниковать при виде виндовых исходников.

Итог

Мы потратили кучу времени не на сам парсинг клиентского кода, а на настройку окружения, в котором этот код живет. Но результат того стоил: наш парсер научился “из коробки” читать реальный код разработчиков на любой операционной системе, не требуя от них сложных конфигураций.

Но “видеть” код — не значит понимать его структуру. Когда парсер наконец-то прочитал все заголовки, мы обнаружили, что он теряет половину методов, если разработчик осмелился определить их за пределами класса (Out-Of-Line). О том, как мы вручную пришивали оторванные методы к шаблонам — в следующем эпизоде.