前言:许多程序员青睐 Visual Studio 或者 Jetbrains IDEs,主要是因为这些开发环境有着很良好的代码提示代码补全代码检查语法高亮。这篇文章将揭开“智能提示”,并给出 VSCode 环境下开发 C/C++ 程序的最佳实践。

1. 编译系统·

很多初学者可能只用过单文件编译,比如把所有的函数都写在一个文件里,然后通过 main 作为 entry 进行调用。后面再做复杂项目的时候,就已经用上了 IDE,因此对于 IDE 做了哪些工作以及为什么要做这些工作,程序员缺乏理解。

一般我们认为编译系统由下面四个部分组成:

  • 前处理(PreTask)
    • 编译前需要拉取最新代码,比如 build-root 编译系统中需要拉取 package 对应的最新代码库或指定哈希值的提交版本。
  • 编译链接(Task)
  • 后处理(PostTask)
    • 某个目标编译完成,需要将之移动(move)到某个特定目录以便于执行下一步操作。
    • 改变某个文件的权限标记。
  • 可选的打包与安装
    • packaging
    • install

1.1 现有的编译组织总结·

  • 第 0 代,(shell) 直接将编译指令编写到 shell 脚本。弊端:
    • 前面的指令出错,后面的指令也会出错
    • 无法按需编译
    • 尽管第 0 代方案可以通过编写复杂的 shell 脚本实现高级方案的某些功能,但是难度较大
  • 第 1 代,(makefile) 定义 target 和 dependency. 弊端:
    • 需要手写依赖
    • 依旧需要写编译指令
    • 无法跨平台
  • 第 2 代,(qmake) 以目标为中心进行描述,需要描述目标之间的依赖关系。弊端:
    • 大工程多模块之间的依赖关系需要手动声明
    • 被链接的模块无法导出宏定义、头文件路径、需要链接的库,导致二次开发很困难,链接工作经常出错。
  • 第 3 代,(cmake) 以模块为中心进行描述
    • cmake 通过 PUBLICPRIVATE 等修饰符,可以给 module 添加属性,并且属性的传递性可在 cmake 中定义。
1
2
3
4
5
6
7
8
9
add_library(module STATIC)
target_sources(module "module/export.cpp")
target_include_directories(module PUBLIC include_path)

add_executable(exe)
target_sources(exe "main.cpp")

# exe 只需要引用模块名即可,不需要 include 某个具体目录
target_link_libraries(exe module)

第三代编译系统以模块为中心,可以 export 路径和库,依赖关系不需要声明。

  • 源代码:顾名思义,就是所需要的代码
  • 导出路径:我构建了一个模块(一堆 .h.c 文件),如果别的模块需要引用本模块的代码、数据结构,则本模块需要将路径导出。
  • 引用路径
  • 公共依赖
  • 私有依赖

每个条目对应的语句如下表所示。

含义 Cmake gn
源代码 sources sources
导出路径 include_directories(PUBLIC) public_configs>include_dirs
公共依赖 link_libraries(PUBLIC) public_deps
私有依赖 link_libraries(PRIVATE) deps
引用路径 include_directories(PRIVATE) include_dirs
宏定义 compile_definitions defines
编译选项 compile_options cflags_cc
链接选项 link_options ldflags

图 1. cmake_progress

1.2 现代 C/C++ 开发需要 IDE·

在裸文本编辑器环境下开发是非常困难的,VSCode 能得以快速发展,主要得益于能给用户体提供开箱即用的良好开发体验。

一般认为,良好的编辑器对某一门语言要提供如下功能:

  • 语义级别的语法高亮
  • 基于上下文的代码提示
  • 基于上下文与关键字的自动补全
  • 对大项目、复杂项目的快速响应

微软 VSCode 自带的 IntelliSense 能实现代码补全,但是在大型 C++ 项目中它的表现不够好,相对比较卡顿,影响使用体验。因此我一般在 settings.json 中添加一句

1
"C_Cpp.intelliSenseEngine": "disabled"

以屏蔽 C/C++ 项目下微软 intelliSense 功能。

1.3 clangd 介绍·

关闭了微软 IntelliSense 之后,我们该如何使用 C/C++ 的代码提示、语法高亮?答案就是 clangd. 下面摘录一段来自 LLVM 官方网站的介绍:

clangd understands your C++ code and adds smart features to your editor: code completion, compile errors, go-to-definition and more. 翻译:clangd 理解你的 C++ 代码同时可以给你的编辑器添加如下功能:代码补全、编译报错提示、定义跳转等。

1.4 clangd 配置·

clangd 提供代码高亮、关键字跳转等服务并非基于 AI,而是基于一套机械化的编译过程的中间产物compile_commands.json. 在使用 cmake 生成 ninja/make 编译脚本 build.ninja/Makefile 的时候,cmake 会顺带生成一个 compile_commands.json,该文件事无巨细地将如何编译整个项目记录了下来,clangd 会根据这个文件的指示去理解源码里的关键字、上下文,而非简单地进行关键字匹配。

注意:一般来说,cmake 在生成 build 目录时会同时生成 compile_commands.json 文件,但有时候也不会生产。使用 cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 . 命令可以强制生成该文件。

如果你的项目只有 Makefile 而没有 CMakeLists.txt,可以通过 compiledb 命令进行转化:

1
2
3
4
5
# 安装
pip install compiledb

# 使用,编译时,在 make 前面加一个 compiledb 命令
compiledb -n make

随后即可在文件夹里看到 compile_commands.json.

使用 cmake 编译 C/C++ 项目时,一般会先新建一个名为 build 的文件夹,然后所有的中间产物、target 都生成在这个文件夹里,compile_commands.json 亦不例外。为了防止 clangd 漫无目的地搜索该文件,我们需要指定它去 ${workspaceFolder}/build 目录里搜索。可通过编辑项目根目录下的 .vscode/settings.json 文件实现此目的。

1
2
3
4
5
6
7
// filename: ${workspaceFolder}/.vscode/settings.json
{
// 添加这一行参数
"clangd.arguments": [
"--compile-commands-dir=${workspaceFolder}/build"
]
}

2. Linux 源码阅读与编译·

Linux 内核源代码作为大型开源项目,比较适合拿来检测 clangd 的分析速度。

2.1 拉取源代码·

如果你的网络质量比较好,机器性能比较高,可直接通过 git 下载最新的源代码:

1
git clone https://github.com/torvalds/linux.git

注意,直接在 linux git 仓库上操作开销很大,因为源码非常庞大,git checkout 一次都会有非常大的时间开销。因此,对于绝大多数不在意最新 commits 的用户,建议从 kernel.org 下载某个特定发行版的压缩包并解压到本地。

对于 zsh 用户,如果你 clone 了整个 linux 仓库,那么最好关闭 zsh git 插件。具体命令如下,该操作只会对这一个仓库生效,配置保存在 ${linux}/.git/config 中,不会对本机的其他 git 文件夹造成影响。--add 参数指的是添加配置项时新起一行,而且不对现在的既有配置做任何更改,因此这项操作比较安全。

1
2
git config --add oh-my-zsh.hide-status 1
git config --add oh-my-zsh.hide-dirty 1

2.2 生成 compile_commands.json·

Linux 内核作为一个很完善的项目,在其 scripts/clang-tools 目录里提供了生成 compile_commands.json 的 Python 脚本:gen_compile_commands.py. 在使用 clang 编译完内核之后,就可以直接执行该脚本,随后将在项目根目录得到 compile_commands.json.

1
2
3
4
5
6
7
8
9
# 切换到最新的发行版
git checkout v6.3

# 以默认配置编译 linux 内核,确保不出错
make CC=clang defconfig
make CC=clang -j16

# 生成 compile_commands.json
./scripts/clang-tools/gen_compile_commands.py

随后,将该项目的 VSCode clangd 配置改为如下:

1
2
3
4
5
6
7
// filename: ${workspaceFolder}/.vscode/settings.json
{
// 添加这一行参数
"clangd.arguments": [
"--compile-commands-dir=${workspaceFolder}"
]
}

现在即可开始使用!

注意:截至 2023 年 8 月 27 日,VSCode 自动安装的 clangd 是 16 版,debian 12 上游 apt 库中最新的 clangd 是 14 版本 (亦可通过 apt install clangd-15 安装更新的版本)。可通过下述命令安装 debian 上游提供的 15 版本,优点是与 apt 管理紧密集成。

1
sudo apt install llvm-15 clangd-15

2.3 阅读源码·

Just enjoy it.

总结·

clangd 是现在各大编辑器 Language Server 的首选工具,在开源发展中起到了巨大的作用。本文抛砖引玉地介绍了 clangd 的基础用法,希望读者能快速上手。VSCode 虽然能开箱即用、快速上手,但是要精通其配置可谓是困难重重,笔者日常也经常感叹微软何能构建如此复杂的编辑器?唯有坚持探索,才能不断提升对软件、代码的理解力。