微软出品的 VS Code 官方 C/C++ 插件当然很好,似乎在 C/C++ 用户中也是用得较为广泛的,调试功能很好用。然而在稍大一点的项目中,有时会出现找不到代码索引的问题,以及代码补全、提示不够智能等,在一定程度上 Clangd 就能够解决这个问题。
Clangd 是 LLVM 社区的一个项目,是一个基于 LSP Language Server Protocol 的 C/C++ 语言服务器(类似于 rust-analyzer、gopls 等),与编辑器插件和其他工具结合使用,能够提供错误提示、代码补全、重构、语法高亮等功能。
对比
以下部分选取之前的课程实验 uCore 代码,对 Clangd 15.0.0 和微软 C/C++ 插件的对应部分功能进行比较。
注:C/C++ 插件也能够读取 compile_commands.json
,但索引很慢,且对于 JSON 中没有覆盖到的源文件,并不能推测符号位置,并不是完全不能找到定义。然而即使找到,提示功能也逊于 Clangd。
安装
首先你需要安装 Clang,macOS 的 Xcode 命令行工具自带,Linux 可以通过包管理器安装(也可以只安装 clang-tools),Windows 则可以使用 llvm-mingw 中的 Clang。个人建议有条件的话使用一个较新的版本,特性会有显著的差异。
关于 Windows 下安装 llvm-mingw
- 从上述 GitHub 页面的 releases 中选择一个版本,下载 assets 中适合你电脑处理器的版本压缩包(如
llvm-mingw-yyyymmdd-ucrt-x86_64.zip
)
- 将该压缩包所有内容解压到电脑上你喜欢的位置
- 将含有数百个 exe 那个文件夹 加入到环境变量中(通常路径含有
bin
)
- 打开你的终端,如果运行
clang -v
显示与刚刚下载所匹配的版本,则安装完成
在 Windows 和 macOS 下,Clang 默认能够模拟 GCC 命令,因此无需再安装 GCC。
然后你当然需要 VS Code,以及 clangd 插件,但没有必要卸载官方 C/C++ 插件。但如果确实安装了,需要在 VS Code 的设置(文件-首选项-设置 或 Code-首选项-设置)中禁用其 IntelliSense 代码补全(C_Cpp.intelliSenseEngine
设置为 disabled
)。
解析、索引文件
Clangd 并不是强在可以完全自动找出索引,而是可以通过方便的方式生成索引。很多简单的项目可以直接使用类似 clang main.c
之类的一条命令编译,此时 Clangd 也可以正常找到引用的所有文件和符号,下面这一段也就暂时没用了。
然而复杂的项目则不然。就拿阿里巴巴前两年一篇 HPCC 论文的 代码 来说,ns-3 本身就是一个十分复杂的项目,仿真程序的引用也弯弯绕绕,也总不能把头文件全部装到系统目录中。虽然有较为方便的编译运行方式,但 Clangd 就不能自动处理所有引用了。因此下面介绍了几种办法,供各位参考。
给 Clangd 传入参数
就像 GCC/Clang 一样,可以将 -I
等参数传入 Clangd,来指定它寻找头文件的范围。VS Code 可以对每个工作区设定单独的 Clangd 参数(clangd.arguments
),这样每个项目的配置都可以不同了。
虽然小项目手动传命令确实不麻烦,但小项目也用不着手动索引呀。所以这个办法其实不是特别方便。
使用 Bear 生成 compile_commands.json
compile_commands.json
记录了每个源文件的编译命令。例如 HPCC 代码的(该 JSON 仅供参考,不同版本生成工具格式有所改变):
[
{
"command": "c++ -c -O0 -ggdb -g3 -std=gnu++11 -Wno-error=deprecated-declarations -fstrict-aliasing -Wstrict-aliasing -fPIC -pthread -Isimulation/build -Isimulation -I/usr/include/libxml2 -DNS3_ASSERT_ENABLE -DNS3_LOG_ENABLE -DHAVE_PACKET_H=1 -DHAVE_SQLITE3=1 -DHAVE_IF_TUN_H=1 -o src/wimax/model/ss-scheduler.cc.1.o simulation/src/wimax/model/ss-scheduler.cc",
"directory": "simulation/build",
"file": "simulation/src/wimax/model/ss-scheduler.cc"
},
{
"command": "c++ -c -O0 -ggdb -g3 -std=gnu++11 -Wno-error=deprecated-declarations -fstrict-aliasing -Wstrict-aliasing -fPIC -pthread -Isimulation/build -Isimulation -I/usr/include/libxml2 -DNS3_ASSERT_ENABLE -DNS3_LOG_ENABLE -DHAVE_PACKET_H=1 -DHAVE_SQLITE3=1 -DHAVE_IF_TUN_H=1 -o src/wimax/model/wimax-mac-queue.cc.1.o simulation/src/wimax/model/wimax-mac-queue.cc",
"directory": "simulation/build",
"file": "simulation/src/wimax/model/wimax-mac-queue.cc"
},
{
"command": "c++ -c -O0 -ggdb -g3 -std=gnu++11 -Wno-error=deprecated-declarations -fstrict-aliasing -Wstrict-aliasing -fPIC -pthread -Isimulation/build -Isimulation -I/usr/include/libxml2 -DNS3_ASSERT_ENABLE -DNS3_LOG_ENABLE -DHAVE_PACKET_H=1 -DHAVE_SQLITE3=1 -DHAVE_IF_TUN_H=1 -o src/wimax/model/bs-scheduler-simple.cc.1.o simulation/src/wimax/model/bs-scheduler-simple.cc",
"directory": "simulation/build",
"file": "simulation/src/wimax/model/bs-scheduler-simple.cc"
},
......
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
[
{
"command": "c++ -c -O0 -ggdb -g3 -std=gnu++11 -Wno-error=deprecated-declarations -fstrict-aliasing -Wstrict-aliasing -fPIC -pthread -Isimulation/build -Isimulation -I/usr/include/libxml2 -DNS3_ASSERT_ENABLE -DNS3_LOG_ENABLE -DHAVE_PACKET_H=1 -DHAVE_SQLITE3=1 -DHAVE_IF_TUN_H=1 -o src/wimax/model/ss-scheduler.cc.1.o simulation/src/wimax/model/ss-scheduler.cc",
"directory": "simulation/build",
"file": "simulation/src/wimax/model/ss-scheduler.cc"
},
{
"command": "c++ -c -O0 -ggdb -g3 -std=gnu++11 -Wno-error=deprecated-declarations -fstrict-aliasing -Wstrict-aliasing -fPIC -pthread -Isimulation/build -Isimulation -I/usr/include/libxml2 -DNS3_ASSERT_ENABLE -DNS3_LOG_ENABLE -DHAVE_PACKET_H=1 -DHAVE_SQLITE3=1 -DHAVE_IF_TUN_H=1 -o src/wimax/model/wimax-mac-queue.cc.1.o simulation/src/wimax/model/wimax-mac-queue.cc",
"directory": "simulation/build",
"file": "simulation/src/wimax/model/wimax-mac-queue.cc"
},
{
"command": "c++ -c -O0 -ggdb -g3 -std=gnu++11 -Wno-error=deprecated-declarations -fstrict-aliasing -Wstrict-aliasing -fPIC -pthread -Isimulation/build -Isimulation -I/usr/include/libxml2 -DNS3_ASSERT_ENABLE -DNS3_LOG_ENABLE -DHAVE_PACKET_H=1 -DHAVE_SQLITE3=1 -DHAVE_IF_TUN_H=1 -o src/wimax/model/bs-scheduler-simple.cc.1.o simulation/src/wimax/model/bs-scheduler-simple.cc",
"directory": "simulation/build",
"file": "simulation/src/wimax/model/bs-scheduler-simple.cc"
},
......
]
|
正因为 Clangd 同时也可以识别 Clang 的参数,所以通过提供编译命令,Clangd 可以自动获取其引用的源代码所在的目录,从而处理引用,以及一些其他的功效。这个文件一般也没人手搓,太麻烦了。
虽然 C/C++ 生态的构建系统千变万化,但是仍然可以用 Bear 截取编译命令。不同版本 Bear 使用方式不同,需要预先注意:
# 较老版本
bear <compile_command>
# 较新版本
bear -- <compile_command>
1
2
3
4
|
# 较老版本
bear <compile_command>
# 较新版本
bear -- <compile_command>
|
这样跑过一次编译之后,就在当前目录生成 compile_commands.json
文件了。
使用 CMake 生成 compile_commands.json
如果整个项目使用 CMake 来构建,可以添加这个参数来生成它:
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1
1
|
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1
|
CMake 会将该文件存放至编译产物的目录中。如果其所处目录并不是在 build/
中,那么需要手动将其移到项目根目录中。