vscode/vim+clangd 环境中实现正确索引交叉编译链中系统头文件路径

(本文假定读者已经对VSCode、LSP、clangd等相关知识有所了解,如果读者对相关内容尚未熟悉,建议先学习VSCode+LSP原理,再学习clangd插件的完整用法,clangd插件官方指导网站网站为:https://clangd.llvm.org,强烈建议认真完整的学习该网站的所有内容!)

自从谷歌创造了V8引擎(演化出node.js,他是VSCode和coc.nvim插件的基础),微软创造了VSCode和LSP协议,苹果创造了clang和clangd,C/C++编辑器领域就发生了翻天覆地的变化,先是VSCode + Remote-SSH + clangd插件通过LSP和clangd语言服务器实现了先进的 “语法级” 跳转 + 补全 + 高亮 + 语法静态解析 + 源码格式化体验,后是vim世界通过移植VSCode源码实现重量级的coc.nvim插件(从此vim几乎可以和VSCode实现同步进化)。目前几乎所有的编辑器都在利用LSP生态在快速的打造全新的功能特性。个人通过一段时间学习和配置,目前发现这套环境用来查看Linux内核源码简直是神一般的存在。

clangd之所以可以完美的匹配Linux内核项目主要原因是内核的Makefile维护的非常好的同时内核本身是一个完全自包含的项目,它对外界的依赖非常小(几乎所有的功能和函数库都在项目内部实现),因此只要产生正确的compile_commands.json就几乎可以实现100%准确的跳转等能力。

不过除了内核,日常工作中接触更多的还是业务层相关的项目,作为一个嵌入式从业人员,99%的时间在和交叉编译链打交道。clangd配合PC环境的GCC项目基本上不怎么需要配置就可以表现的比较完美了,但什么事情到了混乱的嵌入式领导都变得更加折腾,clangd也不例外。clangd用于交叉编译项目的时候一个明显的问题就是在默认配置下由于clangd不知道交叉编译链的位置, 因此也无法准确的知道编译器自带的系统头文件位置,此时默认的动作是将/usr/include等GCC默认系统头文件路径作为搜索路径,带来问题有两个:一是源码文件中include的.h文件跳转不准确,二是由于系统头文件的定义差异,导致源码中很多地方的变量等定义都可能解析失败,最终导致在稍微复杂点的项目中局部变量定义都可能跳转异常,这谁能忍!

目前解决该问题最简单的方法是通过给clangd传递–query-driver 启动参数来通知clangd交叉编译器的位置,clangd在启动时会利用传递的交叉便器参数自动解析出交叉编译链中所有系统头文件的路径(解析原理稍后分析)并自动玩过编译惨数的修改。

VScode中实现项目级–query-driver参数的传递可以通过在项目根目录的 “.vscode/settings.json” 文件添加实现,配置类似如下:

{
    "clangd.arguments": [
        "--all-scopes-completion",
        "--completion-style=detailed",
        "--query-driver=/home/boddy/t7linux-auto/ext-toolchain/bin/arm-linux-gnueabi*"
    ]
}

其中/home/boddy/t7linux-auto/ext-toolchain/bin/arm-linux-gnueabi*表示按需要自动解析交叉编译链bin目录下所有arm-linux-gnueabi开头的交叉编译指令,如果你明确的知道项目使用的唯一交叉编译指令(如:arm-linux-gnueabi-g++)则也可以直接将–query-driver 指定为 /home/boddy/t7linux-auto/ext-toolchain/bin/arm-linux-gnueabi-g++"。

vim + coc.nvim环境如果没有用coc-clangd插件(直接在~/.vim/coc-settings.json全局配置中添加clangd作为c家族语言的languageserver),则对应项目级配置文件为项目根目录下的 “.vim/coc-settings.json”,通过:CocLocalConfig可以直接创建并编辑,配置类似如下:

{
	"languageserver": {
		"clangd": {
			"args": [
				"--all-scopes-completion",
				"--completion-style=detailed",
				"--query-driver=/opt/gcc-linaro-7.3.1-2018.05-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf*"]
		}
	}
}

如果一个简单的–query-driver 参数就可以彻底解决问题,那还算美好, 可惜嵌入式领域总是充满了坎坷。不幸的事情发生在使用复旦微的/opt/gcc-linaro-7.3.1-2018.05-x86_64_arm-linux-gnueabihf这个交叉编译链上。现象很简单,–query-driver 参数配置后基本无效,所有头文件还是自动跳转到/usr目录下。

通过查看clangd的输出日志,找到如下异常报错点:
在这里插入图片描述

报错的意思大概是说解析系统头文件扩展时起始标记未找到,成功配置的日志类似如下:
在这里插入图片描述
由于出错时,日志中出现了中文,凭借着敏锐的直觉,初步判断这可能和系统的中文环境有关,通过将Ubuntu系统的语言修改成英文后重新测,最终确定该问题是由于系统的中文环境导致。引起该问题原理解析如下:

clangd使用–query-driver 参数的原理就是的通过配合compile_commands.json文件中交叉编译指令,通过自动执行类似arm-linux-gnueabi-g++ -v命令来获得交叉编译器的基本信息。出错时出现的中文原因是部分较高版本的交叉编译器支持输出时根据shell的语言环境输出对应的语言翻译结果信息,测试截图如下:
在这里插入图片描述

其中,第一个arm-linux-gnueabi-g++由于版本较低不支持系统语言匹配,因此总是输出英文信息,而第二个arm-linux-gnueabihf-g++由于版本较高支持了系统语言匹配,在中文环境下,部分格式化的输出结果自动转换成了中文。通过临时修改语言配置,可以实现临时将输出结果修转换会英文。

目前clangd最新的版本为15.0,很多特性还处于研发阶段,很明显,–query-driver 参数的解析目前没有考虑到多国语言环境的场景,默认只匹配英文输出。因此当指令结果出现中文时,就意外鸡鸡了。

该问题最简单的解决办法就是将操作系统的环境直接彻底修改为英文。不过直接将系统语言修改到英文有点暴力,而且面对一个满屏英文的系统,看习惯了中文的我还是非常不适应的,经过一晚上的摸索,最终还是找到了一个相对可行的替代方法,现将该方法原理和用法记录如下:

通过对比–query-driver 增加前后clangd的日志输出不难发现,实际上–query-driver 参数的目的就是通过找到交叉编译链并自动执行一些编译链的测试命令找到所有的系统头文件的所在路径,然后在语法树解析阶段将这些路径通过-isystem参数传递给clangd。对应解析截图如下:
在这里插入图片描述
截图中“1”的位置明显是解析获得了所有交叉编译链中的系统头文件检索路径,“2”位置则是在成功解析到路径后通过-isystem传递给了clangd,其中除了传递所有的系统头文件检索路径外,还额外传递了–target和-driver-mode参数。

知道原理后就可以想办法自己实现自动化。自动化思路为通过解析compile_commands.json自动获取正确的交叉编译器,通过编译器的一些测试指令获取当前项目使用的交叉编译器环境中的系统头文件路径集,最终自动将路径集批量添加-isystem头后导入到clangd的项目级配置文件中(项目根目录下的.clangd文件),另外我们还可顺便添加一下-driver-mode参数(–target参数貌似作用不大)。

通过几个小时编写测试,最终实现初版的自动化解析脚本(命名为gen_sys_inc.sh):

#!/bin/bash

compiler=`head -n 10 ./compile_commands.json | grep -Pzo "\"arguments\".*\n\s*\".*\n"|grep -av "arguments"|head -1|awk -F\" '{print $2}'`

if [ "$compiler" = "" ];then
	echo "compile_commands.json文件中没有找到有效的编译器参数"
	return
fi

if [ "`echo $compiler|grep ++`" = "" ]; then
	out="--driver-mode=gcc, "`$compiler -xc /dev/null -E -Wp,-v 2>&1 | sed -n 's,^ ,,p'|awk '{print "-isystem, "$0","}'`
else
	out="--driver-mode=g++, "`$compiler -xc++ /dev/null -E -Wp,-v 2>&1 | sed -n 's,^ ,,p'|awk '{print "-isystem, "$0","}'`
fi

echo -e "CompileFlags:\n\tAdd: ["${out/%,}"]" > .clangd

使用方法:

  1. 将gen_sys_inc.sh脚本加入到path环境路径中去并赋予执行权限以方便任意位置直接调用
  2. 任意项目通过bear或者compiledb工具正确生成项目的compile_commands.json文件
  3. 在compile_commands.json所在目录直接执行gen_sys_inc.sh脚本
  4. 查看在当前目录自动生成的.clangd配置文件是否正确并打开项目C/C++源码测试跳转是否准确即可

注:由于.clangd文件是clangd自身的配置文件,和vim、vscode无关,因此采用gen_sys_inc.sh自动生成.clangd的方式配置项目额外带来了一个好处就是统一了vim和vscode在配置上的差异。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐