Post

Clang & Clangd

Clang & Clangd

Modern C/C++ development benefits greatly from rich editor features: go-to-definition, auto-complete, inline diagnostics, and find-all-references. These features are powered by a language server running in the background. For C and C++, that language server is clangd — the server component of the Clang compiler toolchain.

This post covers what Clang and clangd are, how clangd builds its index, and how to configure it correctly — especially for cross-compilation targets like ARM Cortex-M embedded projects.


Clang vs GCC

Clang is a C/C++/Objective-C compiler front-end built on the LLVM compiler infrastructure. It is an alternative to GCC (arm-none-eabi-gcc, gcc, etc.).

1
2
3
4
5
6
7
8
9
10
Source (.c/.cpp)
       │
       ▼
  Clang Front-End  ─── AST (Abstract Syntax Tree)
       │
       ▼
  LLVM Middle-End  ─── Optimization passes
       │
       ▼
  LLVM Back-End    ─── Machine code (.o)

Key components of the Clang/LLVM toolchain:

ToolRole
clangCompiler binary (replaces gcc)
clang++C++ compiler (replaces g++)
clang-formatCode formatter
clang-tidyStatic analysis / linter
clangdLanguage server (LSP)

In embedded projects you typically keep using arm-none-eabi-gcc to build, but use clangd purely for editor intelligence. clangd reads your build flags and provides IDE features without replacing your compiler.


What is clangd?

clangd is a Language Server Protocol (LSP) implementation.

1
2
3
4
5
6
7
8
9
   Editor (VS Code / Neovim)
          │
          │  LSP (JSON-RPC over stdio / TCP)
          ▼
        clangd
          │
          ├── Reads compile_commands.json
          ├── Parses source files with Clang AST
          └── Builds persistent index in .cache/clangd/index/

clangd provides:

  • Auto-complete — context-aware symbol suggestions
  • Go-to-definition — across files and headers
  • Find references — all usages of a symbol
  • Hover documentation — inline type and doc info
  • Inline diagnostics — real-time error/warning highlighting
  • Include management — unused include warnings

How clangd Indexes Your Project

compile_commands.json

The single most important file for clangd is compile_commands.json. It is a compilation database — a JSON array that tells clangd exactly how each source file is compiled: which compiler, which flags, which include paths, and which defines.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
  {
    "directory": "/home/user/project",
    "arguments": [
      "arm-none-eabi-gcc",
      "-c",
      "-mcpu=cortex-m4",
      "-mthumb",
      "-DUSE_HAL_DRIVER",
      "-DSTM32F412Cx",
      "-ICore/Inc",
      "-IDrivers/CMSIS/Include",
      "Core/Src/main.c",
      "-o", "build/main.o"
    ],
    "file": "Core/Src/main.c"
  }
]

Without this file, clangd has no way to know your include paths or preprocessor defines, leading to false errors everywhere.

Generating compile_commands.json

From a Makefile project, use compiledb:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Install compiledb via pipx for not break system packages
sudo apt install pipx
pipx ensurepath
exec $SHELL
pipx install compiledb

# Wrap your build (actually compiles)
compiledb make -j4

# Dry run: generate compile_commands.json without building
compiledb -n make -j4

# Result: compile_commands.json in the project root

The -n flag is useful when you only need to refresh the database without triggering a full rebuild.

From CMake:

1
2
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ..
# compile_commands.json is generated in the build directory

The Index Cache

Note that if you do not see the indexing process, you can force restart your editor. Once clangd finds compile_commands.json, it:

  1. Parses each translation unit listed in the database using the Clang AST
  2. Builds a persistent index stored in .cache/clangd/index/
  3. Serves editor requests (completions, definitions, etc.) from that index

The index directory contains binary .idx files — one per indexed source or header file:

1
2
3
4
5
6
7
.cache/clangd/index/
├── main.c.B3C63AE1E720F808.idx
├── hello.c.BA49DF66D0DA2B5C.idx
├── hello.h.E2ED0C4AD9EB3836.idx
├── stm32f4xx_hal_dma.c.CB91FD323796E6EC.idx
├── core_cm4.h.7E8338576D541EEB.idx
└── ...

The hex suffix in the filename is a content hash. The index is invalidated automatically when the source file changes.


The .clangd Configuration File

A .clangd file is not required for native C or C++ applications targeting your host machine (e.g., a Linux desktop app, a standard CMake C++ project). In those cases, clangd already knows the target architecture and the system headers, so it works correctly with just compile_commands.json. The .clangd file is specifically needed for cross-compilation scenarios — embedded targets like ARM Cortex-M — where the build flags contain toolchain-specific options that the Clang driver does not understand

For cross-compilation targets like ARM Cortex-M, clangd needs additional tuning because many GCC-specific flags are not understood by the Clang front-end. Place a .clangd file in the project root.

Problem: GCC Flags clangd Does Not Support

When your compile_commands.json contains flags like -mfpu=fpv4-sp-d16, -mfloat-abi=hard, or -gdwarf-2, clangd will emit warnings or fail to parse files correctly because these are GCC-specific and not recognized by the Clang driver.

Solution: .clangd File

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
CompileFlags:
  Add:
    - --target=arm-none-eabi  # Target triple: tells clangd this is an ARM bare-metal project
    - -fshort-enums           # Match GCC enum sizing behavior
  Remove:
    # clangd does not support these ARM GCC-specific flags → remove to avoid false errors
    - -mfpu=*
    - -mfloat-abi=*
    - -mthumb
    - -mcpu=*
    - -Og
    - -g
    - -gdwarf-*
    - -fdata-sections
    - -ffunction-sections
    - -MMD
    - -MP
    - -MF*
    - -Wall

Diagnostics:
  # Suppress warnings that are false positives in cross-compilation context
  Suppress:
    - -Wunused-variable
    - pp_including_mainfile_in_preamble

Key points:

  • --target=arm-none-eabi — tells the Clang driver what target architecture to assume. Without this, clangd defaults to your host machine (x86_64), causing wrong type sizes and missing ARM-specific macros.
  • Remove list — strips flags that cause clangd to error or that it simply does not understand.
  • Suppress list — silences known false-positive warnings that occur because clangd processes headers slightly differently than GCC. -Wunused-variable is suppressed because HAL headers often declare variables that are only used by the compiler under certain #ifdef paths.

Diagnostics and clang-tidy

1
2
3
4
5
6
7
8
9
Diagnostics:
  ClangTidy:
    Add:
      - modernize-*
      - readability-*
    Remove:
      - modernize-use-trailing-return-type
  UnusedIncludes: Strict    # Warn on unused #includes
  MissingIncludes: Strict   # Warn on missing direct includes

VS Code Setup

Important: The VS Code clangd extension is only a UI bridge — it does not ship a language server binary. You must install clangd on your system first, otherwise the extension has nothing to connect to.

1
2
3
4
5
# Install clangd system package
sudo apt install clangd

# Verify
clangd --version

Once clangd is installed on the system, install the clangd extension from the VS Code marketplace (llvm-vs-code-extensions.vscode-clangd). The extension connects to the system clangd binary and visualizes its output inside the editor — completions, diagnostics, go-to-definition, hover. It replaces the built-in IntelliSense engine from the C/C++ extension.

Recommended .vscode/settings.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
    "clangd.arguments": [
        // Allow clangd to query the cross-compiler for system include paths (stdint.h, stdbool.h, etc. from the ARM toolchain, not the host)
        "--query-driver=/path/to/arm-gnu-toolchain/bin/arm-none-eabi-gcc*",
        "--background-index",
        "--clang-tidy",
        "--header-insertion=never",
        "--completion-style=detailed",
        "--log=error"
    ],
    // Disable built-in IntelliSense from the C/C++ extension to avoid conflicts with clangd
    "C_Cpp.intelliSenseEngine": "disabled",
    "C_Cpp.autocomplete": "disabled",
    "C_Cpp.errorSquiggles": "disabled",
    "editor.formatOnSave": true,
    "[c]": {
        "editor.defaultFormatter": "xaver.clang-format"
    },
    "[cpp]": {
        "editor.defaultFormatter": "xaver.clang-format"
    }
}

Key arguments:

  • --query-driver — the most important argument for cross-compilation. It allows clangd to run the actual ARM GCC binary to discover its built-in system include paths (stdint.h, stdbool.h, CMSIS headers, etc.). Without this, clangd cannot find standard headers from the toolchain. Set the path to match your installed toolchain, e.g. /home/user/Downloads/arm-gnu-toolchain-14.3.rel1-x86_64-arm-none-eabi/bin/arm-none-eabi-gcc*.
  • --background-index — build the index in the background when the project opens
  • --clang-tidy — enable clang-tidy checks inline in the editor
  • --header-insertion=never — disable automatic #include insertion (important for embedded projects where include order matters)
  • --completion-style=detailed — show full function signatures in completions
  • --log=error — only log errors; reduces noise in the clangd output panel
  • C_Cpp.intelliSenseEngine: disabled — disables the Microsoft C/C++ extension’s IntelliSense engine. If both are active at the same time, they conflict and produce duplicate or contradictory diagnostics.

Practical Example: ST Bare-Metal Project

Full project layout after setup:

1
2
3
4
5
6
7
8
9
10
11
12
13
project/
├── Core/
│   ├── Inc/
│   └── Src/
├── Drivers/
│   ├── CMSIS/
│   └── STM32F4xx_HAL_Driver/
├── Makefile
├── compile_commands.json    ← generated by compiledb
├── .clangd                  ← flags config for cross-compilation
└── .cache/
    └── clangd/
        └── index/           ← auto-generated by clangd

Step 1 — Install clangd and compiledb:

1
2
sudo apt install clangd
pipx install compiledb

Step 2 — Generate the compilation database:

1
2
3
compiledb make -j4
# or dry-run (no actual build)
compiledb -n make -j4

Step 3 — Create .clangd with the --target and removed flags shown above.

Step 4 — Open the project in VS Code. clangd will start indexing automatically in the background (watch the status bar).

Step 5 — Navigate freely: F12 go-to-definition, Shift+F12 find-all-references, Ctrl+Space completion — all backed by the Clang AST.


Conclusion

clangd gives you compiler-accurate code intelligence because it uses the same Clang AST that the compiler produces. Key takeaways:

  • compile_commands.json is the foundation — without it clangd is blind to your project structure
  • .clangd bridges the gap between GCC cross-compilation flags and what the Clang driver accepts
  • clangd and your build toolchain are independent — you build with arm-none-eabi-gcc but get IDE features from clangd by pointing it at the same compilation database
FilePurpose
compile_commands.jsonTells clangd how every file is compiled
.clangdAdjusts flags for cross-compilation targets
.cache/clangd/index/Persistent symbol index (auto-managed)

This post is licensed under CC BY 4.0 by the author.