Как Visual Studio Code разрешает включения и макросы для автозавершения и сборки?

Вопрос или проблема

Если использовать только расширение C/++ от Microsoft в Visual Studio Code, то как работает препроцессинг include и макросов для автозаполнения? Многие заголовочные файлы include (такие как sys/statvfs.h) зависят от различных макросов для определения различных структур, таких как “statvfs”. Нужно знать значения многих макросов и заменить их все, а также разрешить все пути include, которые сами зависят от конкретных значений различных макросов.

Я попробовал выполнить “cpp main.c” на следующем main.c:

#include <sys/statvfs.h>
int main(int) {
return 0;
}

но полученный вывод не заменяет все макросы, он генерирует свои собственные, такие как:

# 0 "main.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "main.c"
# 1 "/usr/include/x86_64-linux-gnu/sys/statvfs.h" 1 3 4
# 22 "/usr/include/x86_64-linux-gnu/sys/statvfs.h" 3 4
# 1 "/usr/include/features.h" 1 3 4
# 402 "/usr/include/features.h" 3 4
# 1 "/usr/include/features-time64.h" 1 3 4
# 20 "/usr/include/features-time64.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 21 "/usr/include/features-time64.h" 2 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 1 3 4
# 19 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 20 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 2 3 4
# 22 "/usr/include/features-time64.h" 2 3 4
# 403 "/usr/include/features.h" 2 3 4
# 510 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 1 3 4
# 730 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 731 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/long-double.h" 1 3 4
# 732 "/usr/include/x86_64-linux-gnu/sys/cdefs.h" 2 3 4
# 511 "/usr/include/features.h" 2 3 4
# 534 "/usr/include/features.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 1 3 4
# 10 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/gnu/stubs-64.h" 1 3 4
# 11 "/usr/include/x86_64-linux-gnu/gnu/stubs.h" 2 3 4
# 535 "/usr/include/features.h" 2 3 4
# 23 "/usr/include/x86_64-linux-gnu/sys/statvfs.h" 2 3 4


# 1 "/usr/include/x86_64-linux-gnu/bits/statvfs.h" 1 3 4
# 22 "/usr/include/x86_64-linux-gnu/bits/statvfs.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/types.h" 1 3 4
# 27 "/usr/include/x86_64-linux-gnu/bits/types.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 28 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 1 3 4
# 19 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/wordsize.h" 1 3 4
# 20 "/usr/include/x86_64-linux-gnu/bits/timesize.h" 2 3 4
# 29 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4



# 31 "/usr/include/x86_64-linux-gnu/bits/types.h" 3 4
typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;


typedef signed char __int8_t;
typedef unsigned char __uint8_t;
typedef signed short int __int16_t;
typedef unsigned short int __uint16_t;
typedef signed int __int32_t;
typedef unsigned int __uint32_t;

typedef signed long int __int64_t;
typedef unsigned long int __uint64_t;






typedef __int8_t __int_least8_t;
typedef __uint8_t __uint_least8_t;
typedef __int16_t __int_least16_t;
typedef __uint16_t __uint_least16_t;
typedef __int32_t __int_least32_t;
typedef __uint32_t __uint_least32_t;
typedef __int64_t __int_least64_t;
typedef __uint64_t __uint_least64_t;



typedef long int __quad_t;
typedef unsigned long int __u_quad_t;







typedef long int __intmax_t;
typedef unsigned long int __uintmax_t;
# 141 "/usr/include/x86_64-linux-gnu/bits/types.h" 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/typesizes.h" 1 3 4
# 142 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4
# 1 "/usr/include/x86_64-linux-gnu/bits/time64.h" 1 3 4
# 143 "/usr/include/x86_64-linux-gnu/bits/types.h" 2 3 4


typedef unsigned long int __dev_t;
typedef unsigned int __uid_t;
typedef unsigned int __gid_t;
typedef unsigned long int __ino_t;
typedef unsigned long int __ino64_t;
typedef unsigned int __mode_t;
typedef unsigned long int __nlink_t;
typedef long int __off_t;
typedef long int __off64_t;
typedef int __pid_t;
typedef struct { int __val[2]; } __fsid_t;
typedef long int __clock_t;
typedef unsigned long int __rlim_t;
typedef unsigned long int __rlim64_t;
typedef unsigned int __id_t;
typedef long int __time_t;
typedef unsigned int __useconds_t;
typedef long int __suseconds_t;
typedef long int __suseconds64_t;

typedef int __daddr_t;
typedef int __key_t;


typedef int __clockid_t;


typedef void * __timer_t;


typedef long int __blksize_t;




typedef long int __blkcnt_t;
typedef long int __blkcnt64_t;


typedef unsigned long int __fsblkcnt_t;
typedef unsigned long int __fsblkcnt64_t;


typedef unsigned long int __fsfilcnt_t;
typedef unsigned long int __fsfilcnt64_t;


typedef long int __fsword_t;

typedef long int __ssize_t;


typedef long int __syscall_slong_t;

typedef unsigned long int __syscall_ulong_t;



typedef __off64_t __loff_t;
typedef char *__caddr_t;


typedef long int __intptr_t;


typedef unsigned int __socklen_t;




typedef int __sig_atomic_t;
# 23 "/usr/include/x86_64-linux-gnu/bits/statvfs.h" 2 3 4






struct statvfs
  {
    unsigned long int f_bsize;
    unsigned long int f_frsize;

    __fsblkcnt_t f_blocks;
    __fsblkcnt_t f_bfree;
    __fsblkcnt_t f_bavail;
    __fsfilcnt_t f_files;
    __fsfilcnt_t f_ffree;
    __fsfilcnt_t f_favail;
# 48 "/usr/include/x86_64-linux-gnu/bits/statvfs.h" 3 4
    unsigned long int f_fsid;



    unsigned long int f_flag;
    unsigned long int f_namemax;
    unsigned int f_type;
    int __f_spare[5];
  };
# 82 "/usr/include/x86_64-linux-gnu/bits/statvfs.h" 3 4
enum
{
  ST_RDONLY = 1,

  ST_NOSUID = 2
# 113 "/usr/include/x86_64-linux-gnu/bits/statvfs.h" 3 4
};
# 26 "/usr/include/x86_64-linux-gnu/sys/statvfs.h" 2 3 4



typedef __fsblkcnt_t fsblkcnt_t;



typedef __fsfilcnt_t fsfilcnt_t;
# 47 "/usr/include/x86_64-linux-gnu/sys/statvfs.h" 3 4




extern int statvfs (const char *__restrict __file,
      struct statvfs *__restrict __buf)
     __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (1, 2)));
# 73 "/usr/include/x86_64-linux-gnu/sys/statvfs.h" 3 4
extern int fstatvfs (int __fildes, struct statvfs *__buf)
     __attribute__ ((__nothrow__ , __leaf__)) __attribute__ ((__nonnull__ (2)));
# 88 "/usr/include/x86_64-linux-gnu/sys/statvfs.h" 3 4

# 2 "main.c" 2


# 3 "main.c"
int main(int) {
return 0;
}

как обычно кто-нибудь находит полные определения различных структур в C/++? Сделать это все равно приведет к бесконечной паутине директив include, которые придется вручную разрешать, чтобы получить итоговые структуры, так как в большинстве случаев они не так “просты”, как этот.

Как это делает VSCode? Как он строит базу данных для автозаполнения? Как узнать все соответствующие макросы и их выбранные/по умолчанию значения, а также итоговые полные пути для всех include для данного блока трансляции? Для многих системных include существует множество копий в различных местах, с одинаковыми именами или относительными путями. Как я могу точно узнать, какие из них выбирает компилятор или будет выбирать при вызове gcc?

Если я нажму ctrl+space во время редактирования main.c, я сразу же получу все члены statvfs, но это не является решением.

Необходимо знать значения многих макросов и заменить их все, а также разрешить все пути include, которые сами зависят от конкретных значений различных макросов.

Да, так что он это делает.

Более точно, VSCode общается с “языковым сервером” с использованием “протокола языкового сервера”, который сообщает ему свойства объектов, блоков трансляции, модулей C++20 и т.д.

Этот языковой сервер (обычно clangd) является почти полным компилятором/препроцессором, который получает правильные флаги компилятора (обычно из compile_commands.json, сгенерированного, например, с помощью cmake или json). Таким образом, он знает, какие библиотеки используются, и буквально выполняет большинство этапов компиляции (что он не делает – это не записывает двоичный объект и не объединяет их вместе).

Итак, в кратце, вы выполняете полную компиляцию для всего вашего проекта в фоновом режиме, разрешая все вещи, которые вы используете, точно так же, как ваш реальный компилятор. Языковой сервер хранит кэш в памяти, чтобы вам не приходилось постоянно перекомпилировать все, только файл, который вы редактируете, и по мере необходимости. (Это самое инновационное в clangd: это не просто компилятор, который не может обрабатывать “временно некорректный” C, пока вы его набираете, до завершения строки. Он “понимает”, какие части вашей программы не изменяются вашим текущим редактированием, и избегает повторной компиляции этих частей.)

.

Ответ или решение

Visual Studio Code (VSCode) — это мощный инструмент для разработки, который, в связке с расширением C/C++ от Microsoft и языковым сервером, таким как clangd, предоставляет автодополнение кода и помогает в компиляции C/C++ программ. Чтобы понять, как VSCode обрабатывает директивы включения и макросы для автодополнения и сборки, нужно углубиться в детали взаимодействия между этими компонентами.

Теория

Автодополнение в VSCode реализуется через протокол Language Server Protocol (LSP), который позволяет редактору взаимодействовать с языковым сервером. Этот сервер отвечает за анализ кода и его структуру. Он способен предоставить следующие функции:

  1. Анализ синтаксиса: Языковой сервер читает и анализирует код, обрабатывая директивы препроцессора, такие как #include и макросы.

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

  3. Пути поиска и включения: Сервер использует информацию о конфигурации сборки (например, из файла compile_commands.json), чтобы знать, какие библиотеки и заголовочные файлы подключать. Это позволяет ему найти правильные пути к нужным файлам.

Пример

Когда вы начинаете работать над проектом в VSCode и подключаете расширение C/C++, происходит следующее:

  • Первичная настройка: Вы указываете файл конфигурации, обычно compile_commands.json, который может быть сгенерирован системой сборки CMake или вручную настроен. Этот файл содержит все необходимые флаги компиляции, пути к библиотекам и заголовочным файлам.

  • Инициализация clangd: Языковой сервер clangd начинает работу, читая конфигурацию и анализируя ваше рабочее окружение. Он запускает процесс "псевдо-компиляции", разбирая ваш код так, как это делал бы реальный компилятор, но без финальной линковки и генерации бинарного файла.

  • Обработка изменений: При внесении изменений в код, сервер автоматически обновляет свою внутреннюю модель кода. Это позволяет ему быстро предоставлять информацию об автодополнении и находить определения и объявления переменных, классов и структур, непосредственно соотнося их с правильными версиями заголовочных файлов.

Применение

Таким образом, когда вы находитесь в процессе реальной разработки:

  • Автодополнение и навигация: Вы можете использовать автодополнение для быстрого доступа к элементам структуры данных, таким как члены структуры statvfs, без необходимости вручную следить за всеми зависимостями и включениями.

  • Поиск макросов и их значений: VSCode сможет предоставить текущее значение макроса в контексте, в котором вы работаете, поскольку clangd уже обработал его в соответствии с вашей конфигурацией компиляции.

  • Избежание конфликта заголовочных файлов: Языковой сервер точно знает, какие файлы подключить, благодаря информации в compile_commands.json, и не запутается в множестве версий одного и того же заголовочного файла, которые могут существовать в системе.

Заключение

Использование VSCode в связке с clangd для разработки C/C++ проектов предоставляет практически все возможности, необходимые для полного понимания и контроля над зависимостями и макросами. Это позволяет сосредоточиться на логике приложения, в то время как инструменты автоматически обрабатывают инфраструктурные сложности, обеспечивая стабильность и скорость разработки. Важно помнить, что ключом к успешной работе является правильная настройка окружения и конфигурации сборки, что позволяет языковому серверу точно и эффективно выполнять свои функции.

Оцените материал
Добавить комментарий

Капча загружается...