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:
| Tool | Role |
|---|---|
clang | Compiler binary (replaces gcc) |
clang++ | C++ compiler (replaces g++) |
clang-format | Code formatter |
clang-tidy | Static analysis / linter |
clangd | Language 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:
- Parses each translation unit listed in the database using the Clang AST
- Builds a persistent index stored in
.cache/clangd/index/ - 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
.clangdfile 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 justcompile_commands.json. The.clangdfile 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.Removelist — strips flags that cause clangd to error or that it simply does not understand.Suppresslist — silences known false-positive warnings that occur because clangd processes headers slightly differently than GCC.-Wunused-variableis suppressed because HAL headers often declare variables that are only used by the compiler under certain#ifdefpaths.
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#includeinsertion (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 panelC_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.jsonis the foundation — without it clangd is blind to your project structure.clangdbridges 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-gccbut get IDE features from clangd by pointing it at the same compilation database
| File | Purpose |
|---|---|
compile_commands.json | Tells clangd how every file is compiled |
.clangd | Adjusts flags for cross-compilation targets |
.cache/clangd/index/ | Persistent symbol index (auto-managed) |