My Avatar

Vimer-World

Vim is power!

C with YCM

2016.09.02, 发表于 China

如果你对本文有任何的建议或者疑问, 可以在 这里给我们提 Issues, 谢谢! :)

使用YCM写C代码。


VIM是群友都喜欢和擅长的编辑器,YCM(YouCompleteMe)插件通过预编译的方式,为C/C++代码的输入提供了重要的功能:

  1. 成员补齐。结构体和类的成员及方法,YCM能够快速补齐,比ctags的自动补齐速度快很多。
  2. 错误排查。YCM通过Clang工具根据文件的改动进行实时编译,将编译中出现的问题及时反馈。
  3. 定义查找。YCM可以精准的找到某个变量或成员的声明位置。将声明和定义放一起较为合适。

做些准备

YCM插件的安装,依旧是一个老大难的问题,可以在csdn上搜索到相关预编译好的资源(针对win)。Unix类似系统上可以按照YCM的安装说明进行。这里不再介绍YCM的安装。

YCM安装完成后,可以通过:YcmDebugInfo来获得YCM的安装结果,大致如下:

Printing YouCompleteMe debug information...
-- Server has Clang support compiled in: True
-- Clang version: clang version 3.7.0 (tags/RELEASE_370/final)
-- Server running at: http://127.0.0.1:44514
-- Server process ID: 3767
-- Server logfiles:
--   /tmp/ycm_temp/server_44514_stdout.log
--   /tmp/ycm_temp/server_44514_stderr.log

其中较为重要是has Clang support compiled in: True

YCM的插件集成推荐使用Vundle插件。此处贴出一个YCM的配置脚本:

" ycm
Plugin 'Valloric/YouCompleteMe'

" Ycm config}

试试YCM

YCM只针对当前编辑的源文件,它会通过预编译(使用gcc/clang -c)来搜集当前文件的信息。就像正常编译能够检查静态错误一样,YCM通过集成编译环境实现静态编译检查。

错误排查

静态编译检查时,gcc/clang会返回3中提示,分为note,warning和error。YCM会将clang输出的warning和error代码行进行标记,它的效果如下:

|>> 69 #defne container_of(ptr, type, member) ({           \                    
|   70         const typeof( ((type *)0)->member ) *__mptr = (ptr);    \         
|   71         (type *)( (char *)__mptr - offsetof(type,member) );})  
|   72 
invalid preprocessing directive

这里可以看出,YCM检测到第69行有一处错误,这里是拼写错误造成的。

当修改正确后,返回到normal模式,YCM会自动重新编译。如果编译通过,错误标记会消失。

通常不必担心YCM的编译速度,它是在后台完成的,对当前文件的编辑影响不大。

通过实时的静态错误检查,码字过程中可以快速修复,而不必等到执行make时才发现。

成员补齐

当输入.->,YCM会自弹出popmenu,提示需要输入的成员。这个的速度很快,尤其是在编写Linux内核模块代码时,比ctags的c-x,c-o速度快太多。

定义查找

按照上面的脚本配置,在想查找的函数、变量上,按f8可以跳转到声明的地方。模块化的设计通常将声明和定义分开,YCM只能找到声明,无法找到定义,因为定义在其他的源文件中,YCM不关心这些源文件。

定义预编译参数

如果将所有的源文件和头文件放在一个目录下,那个配置好的YCM工作是没有问题的。大多数情况下,头文件是系统自带的标准头文件,或者在其他库的include目录下,这个时候就需要指定如下的预编译命令:

clang -g -O2 -Wall -D_GLIBCXX_USE_CXX11_ABI=0 -I/home/haohuiming/gitlab/tddnet/app -I/home/haohuiming/gitlab/tddnet/libviga/include main.c -o main.o

命令中的clang和源文件的部分需要去掉内容为:

-g -O2 -Wall -D_GLIBCXX_USE_CXX11_ABI=0 -I/home/haohuiming/gitlab/tddnet/app -I/home/haohuiming/gitlab/tddnet/libviga/include

这样就可以让YCM找到需要的头文件及其中的声明了。

这下解决了YCM的预编译参数问题,那么问题是,如果让YCM使用这些参数。在YCM的readme中,有这样一段:

### C-family Semantic Completion Engine Usage

YCM looks for a `.ycm_extra_conf.py` file in the directory of the opened file or
in any directory above it in the hierarchy (recursively); when the file is
found, it is loaded (only once!) as a Python module. YCM calls a `FlagsForFile`
method in that module which should provide it with the information necessary to
compile the current file. You can also provide a path to a global
`.ycm_extra_conf.py` file, which will be used as a fallback. To prevent the
execution of malicious code from a file you didn't write YCM will ask you once
per `.ycm_extra_conf.py` if it is safe to load. This can be disabled and you can
white-/blacklist files. See the _Options_ section for more details.

这段话的意思是,YCM在预编译某个文件时,会首先执行.ycm_extra_conf.py文件中的FlagsForFile来获取额外的编译参数。这意味着想要得到一个完备的YCM集成环境,需要自己手动写这个文件,放在相应的目录下。

通常YCM使用到这里,意味着每个工程都需要一个特殊的py文件,针对不同的源文件,需要不同的预编译参数。需要配置这么多,还得学习python,YCM是让人恼的。

自动生成预编译参数

想要掌握YCM,python免不了需要学习。一般的工程都是通过Makefile编译的,而Makefile中定义了这级目录所有源文件所使用的编译参数,因此可以通过Makefile将编译参数输出到某个文件,然后再使用.ycm_extra_conf.py文件读取该文件并返回编译参数。

例如,在我所使用的所有自动编译的Makefile中,都有这样一段代码:

C_FLAGS_DIR	:= $(CURDIR) $(INCDIRS) $(FOLDERS)
C_FLAGS_DIR	:= $(C_FLAGS) $(foreach dir, $(C_FLAGS_DIR),$(shell test -d '$(dir)' && cd '$(dir)' && echo "-I"`pwd`))	

all: output_cflags $(TARGET)
	
output_cflags:
	$(Q)echo -n "$(C_FLAGS_DIR)" > $(CURDIR)/.ycm_cflags

在这段代码中,C_FLAGS_DIR定义为当前目录、头文件目录和源文件目录的集合。在下一行的处理中,将所有的相对路径转换为绝对路径。YCM不支持相对路径,需要做些调整。

在默认目标all的编译时,将编译参数输出到.ycm_cflags文件中。 C_FLAGS被放到了最前面。

.ycm_cflags创建好之后,就可以通过.ycm_extra_conf.py读取它。

至此,编译参数的集成就完成了。

产生内核模块的编译参数

内核模块在编译的时候,可以选择KBUILD_VERBOSE=1 make来输出编译过程使用的命令。内核编译使用的gcc与clang有很多参数不兼容,因此没法直接使用,需要对支持的参数一项项过滤。

clang可能不支持gcc的很多内联汇编,因此需要对clang的编译结果进行过滤,去掉汇编相关的部分。还有一些其他的问题,这里不再讨论。

一个可用的,编译linux3.16内核模块时的YCM运行状态:

Printing YouCompleteMe debug information...
-- Server has Clang support compiled in: True
-- Clang version: clang version 3.7.0 (tags/RELEASE_370/final)
-- Flags for /home/haohuiming/gitlab/tddnet/libviga/src/vvlink/driver/vvlinkpl.c loaded from /home/haohuiming/gitlab/tddnet/.ycm_extra_conf.py:
-- ['-Wall', '-I/home/haohuiming/gitlab/tddnet/libviga/src/vvlink/driver', '-nostdinc', '-isystem', '/usr/lib/gcc/x86_64-linux-gnu/4.8/include', '-I/usr/src/linux-headers-3.16.0-4-common/arch/x86/include', '
-I/usr/src/linux-headers-3.16.0-4-common/arch/x86/include/generated/uapi', '-I/usr/src/linux-headers-3.16.0-4-common/arch/x86/include/generated', '-I/usr/src/linux-headers-3.16.0-4-common/include', '-I/usr/s
rc/linux-headers-3.16.0-4-common/arch/x86/include/uapi', '-I/usr/src/linux-headers-3.16.0-4-common/arch/x86/include/generated/uapi', '-I/usr/src/linux-headers-3.16.0-4-common/include/uapi', '-I/usr/src/linux
-headers-3.16.0-4-common/include/generated/uapi', '-include', '/usr/src/linux-headers-3.16.0-4-common/include/linux/kconfig.h', '-D__KERNEL__', '-Wall', '-Wundef', '-Wstrict-prototypes', '-Wno-trigraphs', '-
fno-strict-aliasing', '-fno-common', '-Werror-implicit-function-declaration', '-Wno-format-security', '-std=gnu89', '-m64', '-mtune=generic', '-mno-red-zone', '-mcmodel=kernel', '-funit-at-a-time', '-DCONFIG
_X86_X32_ABI', '-DCONFIG_AS_CFI=1', '-DCONFIG_AS_CFI_SIGNAL_FRAME=1', '-DCONFIG_AS_CFI_SECTIONS=1', '-DCONFIG_AS_FXSAVEQ=1', '-DCONFIG_AS_CRC32=1', '-DCONFIG_AS_AVX=1', '-DCONFIG_AS_AVX2=1', '-pipe', '-Wno-s
ign-compare', '-fno-asynchronous-unwind-tables', '-mno-sse', '-mno-mmx', '-mno-sse2', '-mno-3dnow', '-mno-avx', '-O2', '-Wframe-larger-than=1024', '-fstack-protector', '-fno-omit-frame-pointer', '-fno-optimi
ze-sibling-calls', '-pg', '-DCC_USING_FENTRY', '-Wdeclaration-after-statement', '-Wno-pointer-sign', '-fno-strict-overflow', '-Werror=implicit-int', '-Werror=strict-prototypes', '-DCC_HAVE_ASM_GOTO', '-DMODU
LE', '-D"KBUILD_STR(s)=#s"', '-D"KBUILD_BASENAME=KBUILD_STR(file)"', '-D"KBUILD_MODNAME=KBUILD_STR(naklib)"', '-isystem', '/usr/share/vim/bundle/YouCompleteMe/third_party/ycmd/ycmd/../clang_includes']
-- Server running at: http://127.0.0.1:57253
-- Server process ID: 6557
-- Server logfiles:
--   /tmp/ycm_temp/server_57253_stdout.log
--   /tmp/ycm_temp/server_57253_stderr.log

附录

这里贴出我使用的.ycm_extra_conf.py文件内容:

import os
flags = [
'-Wall',
]

if os.path.exists('/usr/src/linux-headers-3.16.0-4-common'):
	kernel_flags = '-nostdinc -isystem /usr/lib/gcc/x86_64-linux-gnu/4.8/include -I/usr/src/linux-headers-3.16.0-4-common/arch/x86/include -I/usr/src/linux-headers-3.16.0-4-common/arch/x86/include/generated/uapi -I/usr/src/linux-headers-3.16.0-4-common/arch/x86/include/generated -I/usr/src/linux-headers-3.16.0-4-common/include -I/usr/src/linux-headers-3.16.0-4-common/arch/x86/include/uapi -I/usr/src/linux-headers-3.16.0-4-common/arch/x86/include/generated/uapi -I/usr/src/linux-headers-3.16.0-4-common/include/uapi -I/usr/src/linux-headers-3.16.0-4-common/include/generated/uapi -include /usr/src/linux-headers-3.16.0-4-common/include/linux/kconfig.h -D__KERNEL__ -Wall -Wundef -Wstrict-prototypes -Wno-trigraphs -fno-strict-aliasing -fno-common -Werror-implicit-function-declaration -Wno-format-security -std=gnu89 -m64 -mtune=generic -mno-red-zone -mcmodel=kernel -funit-at-a-time -DCONFIG_X86_X32_ABI -DCONFIG_AS_CFI=1 -DCONFIG_AS_CFI_SIGNAL_FRAME=1 -DCONFIG_AS_CFI_SECTIONS=1 -DCONFIG_AS_FXSAVEQ=1 -DCONFIG_AS_CRC32=1 -DCONFIG_AS_AVX=1 -DCONFIG_AS_AVX2=1 -pipe -Wno-sign-compare -fno-asynchronous-unwind-tables -mno-sse -mno-mmx -mno-sse2 -mno-3dnow -mno-avx -O2 -Wframe-larger-than=1024 -fstack-protector -fno-omit-frame-pointer -fno-optimize-sibling-calls -pg -DCC_USING_FENTRY -Wdeclaration-after-statement -Wno-pointer-sign -fno-strict-overflow -Werror=implicit-int -Werror=strict-prototypes -DCC_HAVE_ASM_GOTO -DMODULE -D"KBUILD_STR(s)=#s" -D"KBUILD_BASENAME=KBUILD_STR(file)" -D"KBUILD_MODNAME=KBUILD_STR(naklib)"'

def GetMakefileCflags(filename):
		basename = os.path.dirname(filename)
		count = 3
		while count >= 0:
				if os.path.exists(os.path.join(basename, "Makefile")):
						try:
								f = file(os.path.join(basename, ".ycm_cflags"))
								return f.read().split(" ")
						except:
								pass

						return ["-I" + basename]
				count = count - 1
				basename =  os.path.dirname(basename)
		return []

def GetCrossCompileFlags(filename):
		if filename.find("driver") < 0:
				cm = os.getenv("CROSS_COMPILE")
				if cm and cm.find("arm") >= 0:
						return ["-isystem", "/opt/CodeSourcery/Sourcery_CodeBench_Lite_for_Xilinx_GNU_Linux/arm-xilinx-linux-gnueabi/libc/usr/include"]
				return []
		else:
				return kernel_flags.split(" ")

def FlagsForFile( filename, **kwargs ):
		final_flags = flags + GetMakefileCflags(filename) + GetCrossCompileFlags(filename)
		return { 'flags': final_flags, 'do_cache': True }