完美的 c++ 项目代码定义跳转

阅读代码时如果能够很方便地跳转到函数、类型定义处,会极大地提高效率。使用 grep 或 ctags 或 gtags 的问题在于给出的结果不够严谨,可能会给出很多候选,容易打断思路,另一方面 c++ 本身语法非常复杂,继承、重载等会让选择正确的跳转处变得更加困难, ctags 或 gtags 之类的静态代码分析工具对 c++ 的支持也相当有限。

clang 编译器较之 gcc 在进行代码分析方面提供了良好的支持,编译器给出的结果往往是最正确和最丰富的。

本文描述了使用如何将 emacs 打造成阅读 c++ 代码的利器。

编译数据库

编译数据库( compile_commands.json )里面记录了每一个源代码文件对应的编译命令,有了编译数据库就可以从 clang 编译器获取最详尽的代码分析数据,让代码跳转、自动完成更加精确。

不同的构建工具可以使用相应的工具来生成编译数据库( compile_commands.json )。

bazel

  • 安装

    使用 Bazel And Compile Commands 脚本可以很方便地为 Bazel 构建项目生成 compile_commands.json

    git clone https://github.com/vincent-picaud/Bazel_and_CompileCommands.git
    
  • 生成

    ../Bazel_and_CompileCommands/setup_compile_commands.sh
    ../Bazel_and_CompileCommands/create_compile_commands.sh //...
    

make

Bear 是一个生成生成编译数据库的工具,其工作原理是监视编译工具(如:make)调用的编译命令,是一种很通用的方案。

  • 安装

    yaourt -S bear
    
  • 生成

    bear make
    

cmake

  • 安装

    yaourt -S cmake
    
  • 生成

    cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .
    

rtags

RTags is a client/server application that indexes C/C++ code and keeps a persistent file-based database of references, declarations, definitions, symbolnames etc.

rtags 是基于 clang,根据编译器提供的信息可以实现精确的查找定义、引用及调用、列出派生类等。

启动后台服务

nohup rdm &

提交项目信息到后台服务

由于 bazel 会将代码通过软链接放到一个沙箱中进行编译,导致生成的编译数据库中代码目录与实际编辑的路径不一致,使用 rtags 查找定义时会报 "/xxx/xxx.cpp not indexed" 错误,可以通过提交项目信息时指定 --project-root 进行修正。

rc -J . --project-root $PWD

在 emacs 中整合以上工具

请参考 https://github.com/Andersbakken/rtags#elisp

以下是我的配置,首先需要安装这些 emacs 包: rtags helm-rtags company-rtags flycheck-rtags

;;;; rtags
(defun my-rtags-load-compile-commands-command ()
  "rtags load compile_commands.json command"
  ;; compile_commands.json generate by https://github.com/vincent-picaud/Bazel_and_CompileCommands
  ;; will refer source code from bazel's sandbox, must use "--project-root" to fix it.
  (let ((project-root default-directory)
        (tmp-project-root ""))
    (while (and project-root (not (file-exists-p (concat project-root "compile_commands.json"))))
      (setq tmp-project-root (file-name-directory (directory-file-name project-root)))
      (message "tmp-project-root: %s, project-root: %s" tmp-project-root project-root)
      (if (equal tmp-project-root project-root)
          (setq project-root nil)
        (setq project-root tmp-project-root)))
    (unless project-root
      (message "RTags: compile_commands.json not exists")
      (setq project-root default-directory))
    (message "RTags: %s" (concat project-root "compile_commands.json"))
    (format "rc -J %s --project-root %s" project-root project-root)))

(defun my-rtags-run ()
  "rtags startup with generated compile_commands.json"
  (interactive)
  (rtags-start-process-unless-running)
  (shell-command (my-rtags-load-compile-commands-command)))

(defun my-rtags-build ()
  "rtags startup use compile_commands.json generate from build tool"
  (interactive)
  (cond ((file-exists-p "BUILD") (my-rtags-bazel))
        ((file-exists-p "CMakeLists.txt") (my-rtags-cmake))
        ((file-exists-p "Makefile") (my-rtags-make))
        (t (error "No build tool detected"))))

(defun my-rtags-bazel ()
  "rtags startup use compile_commands.json generate from bazel"
  (interactive)
  (let ((tool_dir "~/Opensource/Bazel_and_CompileCommands")
        (command ""))
    (setq command (format "%s/setup_compile_commands.sh; %s/create_compile_commands.sh //..." tool_dir tool_dir))
    (setq command (read-string "Build bazel compile_commands.json: " command nil nil))
    (unless command
      (error "Build compile_commands.json for bazel failed"))
    (rtags-start-process-unless-running)
    (async-shell-command (concat command " && " (my-rtags-load-compile-commands-command)))))

(defun my-rtags-make ()
  "build compile_commands.json for make"
  (interactive)
  (let ((command (read-string "Build make compile_commands.json: " "bear make" nil nil)))
    (unless command
      (error "Build compile_commands.json for make failed"))
    (rtags-start-process-unless-running)
    (async-shell-command (concat command " && " (my-rtags-load-compile-commands-command)))))

(defun my-rtags-cmake ()
  "build compile_commands.json for cmake"
  (interactive)
  (let ((command (read-string "Build cmake compile_commands.json: " "cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ." nil nil)))
    (rtags-start-process-unless-running)
    (async-shell-command (concat command " && " (my-rtags-load-compile-commands-command)))))

(setq rtags-completions-enabled t)
(eval-after-load 'company
  '(add-to-list
    'company-backends 'company-rtags))
(setq rtags-autostart-diagnostics t)
(rtags-enable-standard-keybindings)
(require 'helm-rtags)
(require 'flycheck-rtags)
(define-key c-mode-map (kbd "C-c C-j") 'rtags-find-symbol)
(define-key c++-mode-map (kbd "C-c C-j") 'rtags-find-symbol)
(define-key c-mode-map (kbd "C-c C-b") 'rtags-location-stack-back)
(define-key c++-mode-map (kbd "C-c C-b") 'rtags-location-stack-back)
(define-key c-mode-map (kbd "C-c C-r") 'rtags-find-references)
(define-key c++-mode-map (kbd "C-c C-r") 'rtags-find-references)

第一次需在项目根目录执行 M-x my-rtags-build ,它会先生成编译数据库( compile_commands.json )再启动 rtags 后台服务。以后可直接运行 rtags 后台服务 M-x my-rtags-run

现在随便打开项目中的 c++ 源代码,将光标放到变量名上,然后按 C-c C-j 跳转到定义处,更多用法请参考 rtags 文档。