记一次clangd踩坑的经历,算是对clangd的整个原理有一个初步的了解把。
背景
使用clangd来看Linux内核的代码,发现psi.c
这个源文件没有办法跳转,使用Python生成的compile_commands.json
没有psi这个文件,让我一度认为我在编译的时候没有开启,在我认真检查了之后,我是用nm
查看了生成的最终的vmlinux,发现有这个源文件中的函数符号。
后来我想到,他可能被其他其他源文件包含了,所以我全局搜索了# include "psi.c"
,发现他被包含在build_utility.c
。
那么当时我认为是include之后,编译时候这些符号直接就成了build_utility.c
了,但是我看debug信息发现还是正确的,符号的解析依旧是psi.c
的。
然后我忽然想到,既然psi.c
被include到了build_utility.c
,那么他的头文件是怎么处理呢。然后我就发现他自身没有头文件,这就很好解释了为什么符号能够被正确的认识,但是他自身的源代码,没有办法进行跳转,因为他没有包含任何头文件,就没有办法知道那些数据结构的结构。
clangd原理
编译命令
解释源代码需要一定的上下文。
#include <stdio.h> // 这究竟是哪个文件?
char data[sizeof(int)]; // 这个数组有多大?
@class Foo; // 是 Objective-C,还是只是语法错误?
C++ 编译器期望这些上下文通过命令行标志传递(并提供一些默认值)。一个命令可能看起来像这样:
clang -x objective-c++ -I/path/headers --target=x86_64-pc-linux-gnu -DNDEBUG foo.mm
他首先肯定去一些默认的地方去读取,但是一个复杂的项目他的头文件路径也是非常复杂的。
他的解决方案是为每一个源文件配置一个虚拟编译命令(例如 clang foo.cc -Iheaders/
),他通过解析这个命令来确定他的依赖和配置。理想情况下,这些虚拟编译命令的有构建系统比如cmake这些来实现,他就是告诉了我们每个源文件怎么编译,去哪里找头文件,宏的值是什么。
索引
索引包含了整个代码库的信息,这些信息包括符号等大量信息。
每当我们打开一个新的源文件,他首先解析头文件中的数据,他根据虚拟编译命令,知道了从哪里找到这些头文件,然后递归的处理这些头文件。
然后clangd依赖的是clangd,他会将整个代码解析为ast,笼统的说就是使用编译原理的词法分析、语法分析以及语义分析。然后将类、函数这些符号构建成相应的索引,这样就可以了。
然后我们就能进行各种跳转了。