和「LSP」一起写代码:VS Code + Clangd 编写 C/C++

微软出品的 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++ 插件的对应部分功能进行比较。

Clangd:能够正确找到符号定义对应的源文件,提示较多
Clangd:能够正确找到符号对应的源文件

C/C++:未找到符号定义,仅有返回值提示
C/C++:未找到符号定义 ,仅有返回值提示

Clangd:有较完整的补全列表
Clangd:有较完整的补全列表

C/C++:无法补全
C/C++:无法补全

Clangd:识别到格式错误,并提供修改建议
Clangd:识别到格式错误,并提供修改建议

C/C++:未识别到格式错误
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 仅供参考,不同版本生成工具格式有所改变):

json
 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 使用方式不同,需要预先注意:

shell
1
2
3
4
# 较老版本
bear <compile_command>
# 较新版本
bear -- <compile_command>

这样跑过一次编译之后,就在当前目录生成 compile_commands.json 文件了。

使用 CMake 生成 compile_commands.json

如果整个项目使用 CMake 来构建,可以添加这个参数来生成它:

bash
1
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1

CMake 会将该文件存放至编译产物的目录中。如果其所处目录并不是在 build/ 中,那么需要手动将其移到项目根目录中。

许可证:CC BY-SA 4.0
最后更新于 Mar 02, 2024 22:17 +0800