Emacs 折腾日记(三十)——打造C++ IDE 续

news/2025/9/18 21:27:46/文章来源:https://www.cnblogs.com/lanuage/p/19099761

上一篇博客中,我完成了C++ IDE初步工作,包括代码的高亮、折叠、跳转以及补全等工作。但是作为IDE来说功能还有点不够,就我个人而言作为IDE来说它还需要具备一键编译运行和调试功能。这篇文章就来记录我是如何实现上述功能的

编译运行

我使用的演示项目比较简单,它的文件结构如下:

.
├── include
│   └── head.h
└── src├── add.cpp├── div.cpp├── main.cpp├── mult.cpp└── sub.cpp

它分为两个目录分别保存头文件和源文件。其中头文件只有一个定义各个接口函数,而接口函数的实现就放到各自定义的cpp文件中。这里使用加减乘除的四则运算的实现来作为演示。

这里我分别演示一下Make文件和CMake构建的项目是如何实现一键编译运行的。

Make构建的项目

针对上前面介绍的简单项目,我们可以写出如下的Makefile

# 编译器设置
# 定义项目根目录
ROOT_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
CXX := g++
CXXFLAGS := -Iinclude -Wall -Wextra -pedantic -std=c++11 -MMD -MP
LDFLAGS := 
EXE_OUTPUT := $(ROOT_DIR)bin
TARGET := $(EXE_OUTPUT)/app$(info TARGET = $(TARGET))# 源文件和对象文件设置
SRC_DIR := src
SRCS := $(wildcard $(SRC_DIR)/*.cpp)
OBJ_DIR := $(ROOT_DIR)build/obj
OBJS := $(patsubst $(SRC_DIR)/%.cpp,$(OBJ_DIR)/%.o,$(SRCS))
DEPS := $(OBJS:.o=.d)# 默认目标(第一个目标)
all: $(TARGET)# 链接生成可执行文件
$(TARGET): $(OBJS)@mkdir -p $(@D)$(CXX) $(LDFLAGS) $^ -o $@# 编译源文件并生成依赖
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.cpp | $(OBJ_DIR)$(CXX) $(CXXFLAGS) -c $< -o $@# 创建对象文件目录
$(OBJ_DIR):mkdir -p $@# 包含自动生成的依赖关系
-include $(DEPS)# 清理生成的文件
clean:rm -rf $(TARGET) build.PHONY: all clean

上面我们定义了头文件路径为include 目录,并且规定了中间文件生成在 build/obj 中,最后定义了生成可执行程序在 bin/app

对于编译来说,Emacs内置了 compile 命令,它会自动执行 make -k 命令,但是如果我在使用Emacs的过程中切换到了其他目录的话,还需要特别指定Makefile 所在的路径,对我来说我希望能在尽可能少输入参数的情况下完成同样的操作,不太希望每次都指定项目根目录,好在之前配置的projectile 插件帮助我们识别出来了项目的根目录。所以这里可以使用 projectile-compile-project 来自动指定根目录并编译。

2

从上面的截图可以看到,flycheck 提示了几个错误,这是因为项目没有生成 compile_commands.json文件,所以lsp服务器无法跨文件分析,导致找不到头文件。原始的make 命令并不支持生成 compile_commands.json 文件,我们可以通过 bear 命令来完成这个工作,它的用法比较简单,只需要使用 bear -- <your-build-command> 即可, 对于使用make编译的项目来说 <your-build-command> 代表的就是 make 命令。

我们需要考虑的一个问题是,如何将bear加入到编译命令中,也就是将它自动生成的 make -k 给替换掉,第二个问题是如果当前目录在其他目录下,如何保证 compile_commands.json 永远生成在根目录下

Emacs中有一个变量 compile-command 保存了编译的命令,如果我们使用Emacs自带的compile来编译可以通过修改它来实现,而 projectile-compile-project 则是通过变量 projectile-project-compilation-cmd 来保存编译命令,默认是nil,对于使用 projectile 我们通过修改这个变量的值从而修改编译时使用的命令。另外既然 projectile 可以得到项目的根目录,我们就可以利用这个插件来获取项目的根目录,有了这些信息通过一个函数就可以重新生成一个编译命令

(defun my/general-compile-command()(concat "bear --output " projectile-project-root "compile_commands.json"  " -- make -k"))

这个函数的代码非常简单,通过 projectile-project-root 来获取项目的根目录,然后通过字符串拼接的方式来得到编译命令

3

生成 compile_commands.json 成功之后,我们重启 lsp 服务后可以看到错误都消失了,只有两个警告了

了解了编译的一些情况,下面来看看如何在Emacs中执行生成的可执行程序。

Emacs中可以使用 shell-command 来执行可执行 shell 命令。例如我们可以在项目的根目录下执行 shell-command ./bin/app。很明显如果每次都指定程序的路径是非常麻烦的事,我希望能有一个命令或者函数来自动执行可执行程序。但是Makefile构建的项目比较古老也灵活,Makefile中没有一个固定的方式或者写法来指定可执行程序的生成路径,也就是说没有一个通用的方式来根据Makefile获取可执行文件的路径。一种折中的方案就是针对每个项目都定义一个 execuable-path 的变量来指定可执行程序的路径,然后再通过elisp代码来根据这个变量执行程序

(defun my/run-program()(interactive)(shell-command (concat projectile-project-root executable-path)))

我们可以针对每个项目单独设置一个 executable-path 变量。Emacs会读取项目根目录中的 .dir-locals.el 文件,并且将文件中定义的变量作为项目的局部变量,所以我们只需要在该文件中定义好 executable-path 就可以了。我们可以通过命令 add-dir-local-variable 来往该文件中添加一个局部变量,也可以自己手写该文件实现这一操作

添加完变量之后,项目根目录中的 .dir-locals.el 文件内容如下

;;; Directory Local Variables            -*- no-byte-compile: t -*-
;;; For more information see (info "(emacs) Directory Variables")((c++-mode . ((executable-path . "bin/app"))))

在重启Emacs之后,执行这个函数就可以做到一键运行了

4

有了这些,我希望能将它们有机的组合起来,也就是说按下某个快捷键,这里我暂时定义为 <F7>。它直接同时执行编译和运行的操作。通过 C-<F7> 来完成重编译的操作。

;; 重新编译
(defun my/rebuild-program ()(interactive)(let ((root (file-name-as-directory (projectile-project-root))))(shell-command (concat "make clean -C " root))(setq compile-command (concat "bear --output " root "compile_commands.json" " -- make -k -C " root))(compile compile-command)));; 绑定快捷键
(setq compilation-read-command nil) ;; 取消编译时确定命令行
(evil-define-key 'normal c++-mode-map (kbd "<f7>") #'projectile-compile-project)
(evil-define-key 'normal c++-mode-map (kbd "C-<f7>") #'my/rebuild-program)))

5

6

这里的代码比较简单,对于编译来说只需要将之前执行的 projectile-compile-project 绑定到对应的快捷键;对于重编译,我通过函数 my/rebuild-program 来完成。这个函数主要操作是先执行 make clean 命令然后重新执行 make

在正式绑定快捷键之前,有一句 (setq compilation-read-command nil)projectile-compile-projectcompile 命令都是交互式命令,执行时会首先显示对应的编译命令,需要用户手动执行回车确认命令,这句代码的意思是不取消它们需要确认的步骤,直接执行命令。

本来我打算在重编译函数中也采用 projectile-compile-project 但是它这个交互式我一直取消不了,所以这里我直接采用 compile 指定根目录的方式来完成这个操作。

如果想要绑定一键运行的操作也可以采用这个思路,将快捷键绑定到 my/run-program 函数中,这个函数也可以添加一个编译命令确保执行的是最新代码生成的可执行程序

CMake工程

CMakeLists.txt 文件内容如下:

cmake_minimum_required(VERSION 3.15)
set(CMAKE_CXX_STANDARD 11)
project(test)# aux_source_directory(${PROJECT_SOURCE_DIR} source)
file(GLOB source ${CMAKE_SOURCE_DIR}/src/*.cpp)
include_directories(${CMAKE_SOURCE_DIR}/include)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)
add_executable(app ${source})

这个CMakeLists.txt 文件中主要定义了编译使用到的源文件、头文件目录路径和生成的exe路径

emacs 中有一个名为 cmake-ide 的包,它用于读取cmake配置中的各项参数并将参数传递到对应的包中,虽然用它可以很方便的针对cmake配置,但是它依赖rtags,并且没有支持lsp-mode。所以这里就淘汰它,还是想办法自己实现

针对cmake来说,要生成 compile_commands.json 比较简单,我们可以在命令行中使用

cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1

也可以在cmake配置文件中,project命令之后添加

set (CMAKE_EXPORT_COMPILE_COMMANDS ON)

这里我采用将命令写到cmake文件中的方式。

对于cmake 编译的过程主要由两个部分组成,首先是cmake构建项目生成Makefile,然后使用make 命令编译项目。我们要实现自动编译也需要模拟这两个命令。

与上面类似,这里我只需要将 my/general-compile-command 函数做少许改动即可

(defun my/cmake-general-compile-command ()(concat "cmake -B " (projectile-project-root) "build -DCMAKE_BUILD_TYPE=Debug " (projectile-project-root)" && ln -sf " (projectile-project-root) "build/compile_commands.json " (projectile-project-root) "compile_commands.json"" && cmake --build " (projectile-project-root) "build --config Debug"))

这个函数生成的命令主要完成三个工作,将构建编译生成的临时文件放到 build 目录下;因为生成的 compile_commands.json 文件也一起放在了 build 目录中,所以我加一个软链接到项目根目录的操作;最后就是执行编译操作了。

至于重编译则于上面的步骤相似,cmake一般我习惯删除存放临时文件的build目录然后重新执行cmake构建。所以这里还是模拟这个过程

(defun my/cmake-rebuild-program ()(interactive)(let ((root (file-name-as-directory (projectile-project-root))))(shell-command (concat "rm -rf " root "build"))(setq compile-command (my/cmake-general-compile-command))(compile compile-command)))

至于运行程序,我们还是可以采用上面介绍的指定程序生成路径的方式。也就是不管使用cmake或者Makefile 构建的工程都可以使用上面定义的 my/run-program 函数来运行程序

调试

作为IDE的一个重要或者说基础的功能,调试功能是必不可少的。

emacs 自身支持使用gdb进行调试,我们可以执行 M-x gdb 来启动一个调试示例,这个时候我们一边通过gdb的调试命令来控制程序语句的执行一边观看代码的上下文。但是目前流行的方式是使用 dap 来调试程序,至于什么是dap,我在配置vim的时候已经介绍过了,这里就不再赘述了

emacs 中有一个名为dap-mode 的插件通过这个插件可以实现dap相关的功能。因为在介绍vim配置的时候我使用的是vscode中的 cpptools插件,这里我打算也使用它来作为dap的调试后端,可以通过cpptools官方仓库 进行下载

接着需要安装lldb-vscode,它是针对vscode的一个插件,我们可以在 官方仓库 中找到对应的下载包。

下载完成之后可以直接解压到对应目录,这里我解压到 ~/.emacs.d/cpptools 目录中。此时对应的调试后端程序为 ~/.emacs.d/cpptools/extension/debugAdapters/bin/OpenDebugAD7。我们需要赋予它可执行的权限。

在这些工作都做好之后,可以使用下面的代码来安装dap-mode

(use-package dap-mode:ensure t:after (lsp-mode):config(dap-auto-configure-mode) ; 可选:启用自动配置(setq dap-cpptools-debug-program '("~/.emacs.d/cpptools/extension/debugAdapters/bin/OpenDebugAD7")))

我们可以通过命令 dap-debug-edit-template 创建一个调试的模板。对给出的模板做一些简单的修改

(dap-register-debug-template"cpptools::Run Configuration"(list :type "cppdbg":request "launch":name "cpptools::Run Configuration":MIMode "gdb":program "${workspaceFolder}/bin/hello":cwd "${workspaceFolder}":environment []:miDebuggerPath "/usr/bin/gdb"))

我们执行一下这个代码就会向Emacs注册一个调试的模板。接着直接调用 dap-debug 即可启动调试。

虽然我们可以将上述注册的代码放到主配置文件中,但是其中的一些关键的字段,例如程序的位置,使用的环境变量,以及对应的调试参数都无法做到所有程序都统一,所以这里我觉得还是需要的时候直接修改就好了。

dap-mode 的一些命令如下:

  • dap-debugdap-continue : 启动调试或者运行到下一个断点处
  • dap-next : 执行下一句代码
  • dap-step-in : 执行下一行代码并进入函数内部
  • dap-step-out : 执行到函数返回
  • dap-breakpoint-toggle : 创建或者删除端点

我们可以对这些命令进行键位绑定

(use-package dap-mode:ensure t:after (lsp-mode):config(dap-auto-configure-mode) ; 可选:启用自动配置(require 'dap-cpptools)(setq dap-cpptools-debug-program '("~/.emacs.d/cpptools/extension/debugAdapters/bin/OpenDebugAD7"))(evil-define-key 'normal dap-mode-map (kbd "<f10>") #'dap-next)(evil-define-key 'normal dap-mode-map (kbd "<f9>") #'dap-breakpoint-toggle)(evil-define-key 'normal dap-mode-map (kbd "<f5>") #'dap-debug))

这样我们可以使用上述快捷键来进行调试操作

7

总结

这篇文章花了好长时间才弄出来,主要是我对于emacs和lisp语言不太熟悉,中间在尝试编写一键运行和配置dap时耗费了大量的时候。最终我还是成功了,至少我完成我当初想要的一些ide的基本功能,当然在使用上还是比不过vscode,但是在折腾中总能找到一丝乐趣。

本文中的配置仅仅经过我自己机器的检验,本来想弄的更加灵活更加接近vscode的体验,有一些我自己想要的功能还没加上,仅仅做了一个可用的玩具。但是我没有想到什么办法,而且这篇文章已经憋了好久了,再不写点东西出来我感觉马上就要放弃了,我想先弄点东西出来给自己一个激励,让我有动力继续深入学习一下Emacs的其他内容。等我多学了一点Emacs多写了一点elisp代码之后可能会对调试和编译方面的代码做一个大的更新。

最后如果有读者觉得这篇文章写的有那么一点帮助,那将是我的荣幸,感谢读者在百忙之中能读完本文。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/907440.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构 项目一

一:数据结构的基本概念 数据结构----研究数据的特性及数据之间存在的关系 算法+数据结构=程序。其中数据结构是指数据逻辑结构和物理结构,算法是对数据运算的描述。 用计算机解决一个具体问题时,首先从具体问题中抽…

好烦

我不行了,一做初赛题就有一种莫名其妙的烦躁,根本就写不进去,一点都冷静不下来

完整教程:.NET驾驭Word之力:玩转文本与格式

完整教程:.NET驾驭Word之力:玩转文本与格式2025-09-18 21:15 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: b…

用 Go 语言与 Tesseract OCR 识别英文数字验证码

一、安装与配置 安装 Tesseract OCR 你需要先安装 Tesseract OCR 引擎。具体步骤如下: Ubuntu: 更多内容访问ttocr.com或联系1436423940 sudo apt-get update sudo apt-get install tesseract-ocr macOS: brew instal…

FreeRTOS和LVGL组合使用教程

前言 关于这两者组合使用的教程,网上可以说是各种方法都有,移植的时候我也有遇到各种问题,在此处记录一下解决过程 问题 栈空间的分配问题 FreeRTOS和LVGL的栈分配都尽量多一点,不然后面的任务可能创建失败 lvgl心…

Codeforces 1646 记录

目录C. Factorials and Powers of Two / 阶乘数与二的幂 D. Weight the Tree / 树上赋权 E. Power Board / 乘方表 F. Playing Around the Table / 圆桌打牌C. Factorials and Powers of Two / 阶乘数与二的幂 题意简述…

综合与实现流程【p3】--(DSP-存储)优化PS系统集成

(一)资源优化 1 DSP优化 创建优化的DSP映射 创建文件 dsp_optimized_pe.v: `timescale 1ns / 1ps ////////////////////////////////////////////////////////////////////////////////// // 优化的PE模块 - 直接使…

Linux中 sed命令忽略大小写匹配

001、[root@localhost test]# ls a.txt [root@localhost test]# cat a.txt ## 测试数据 22 abc44 88 32 ABC11 43 14 aBc44 86 [root@localhost test]# sed -n /abc/p a.txt ## 匹配abc 22 abc44 88 [r…

VISA Resource name

VISA Resource name📌 步骤放置 VISA Open在 Block Diagram 放一个 VISA Open 节点。Resource name 输入 TCPIP0::192.168.2.121::inst0::INSTR(就是你之前的地址)。VISA Write在 VISA Open 的 session 输出连到 V…

【STL库】哈希封装 unordered_map/unordered_set - 教程

【STL库】哈希封装 unordered_map/unordered_set - 教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas&…

Docker 常用命令详解与参数说明 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

7zip压缩解压缩-测试CPU性能

前言全局说明在B站刷大佬视频的时候,看到UP在Debign linux 上用7z测试CPU性能,惊讶,7z还有这功能。 赶快打开手边的电脑,看看 Windows 上的 7z 有没有这功能,一用,果然有这个功能。 以前想知道CPU性能就只能装些…

交叉编译openharmony版本的gdb

1465 cd ../gmp-6.3.0/ 1466 ./configure --prefix=/system/ CC=aarch64-linux-gnu-gcc --build=`./config.guess` --target=aarch64-linux-gnu --host=aarch64-linux-gnu 1467 make 1468 …

高数

1 求 \(\lim_{n\to\infty}\left(1-\frac 1n\right)^{n^2}\) 解: 首先证明 \(\lim_{n\to\infty}\left(1-\frac 1n\right)^{n}=e^{-1}\)。 \[\begin{align*} \lim_{n\to\infty}\left(1-\frac 1n\right)^{n} &=\lim_…

06-排序操作

06-排序操作$(".postTitle2").removeClass("postTitle2").addClass("singleposttitle");介绍 排序操作很常用,比如查询学员成绩,按照成绩降序排列。排序的SQL语法: select .. from .…

P5666 [CSP-S2019] 树的重心

分为 \(x \ne rt\) 和 \(x = rt\) 两种情况计算. 对于第一种情况,不难发现我们合法的裁减下来的连通块大小是在一个区间范围之内的,于是 DFS 时用一棵树状数组修改即可(因为这个大小可能是子树大小可能是子树外大小,这…

Java运行机制

Java 程序运行机制 编译型(compile) 解释型 程序运行机制 ![机制图](C:\Users\asus\Desktop\图集\屏幕截图 2025-09-18 204707.png)

除自身以外数组的乘积-leetcode

题目描述 给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 请 …

【2022】SDRZ夏令营游记

为什么2022的游记会在2025年发? 因为感觉洛谷博客快扛不住了,决定开始搬运。今天是夏令营最后一天了,在机房里坐不住了,写篇游记来纪念一下。 day0: 这天是gryz65级三个校区的信竞同学第一次大会师。我成功与一区…

rapidXML解析xml文件

1.rapidXML介绍 RapidXML 是一个轻量级、高性能的 XML 解析库,以单头文件形式提供(rapidxml.hpp 及辅助头文件),适合在 C++ 中解析中小型 XML 文档。获取 RapidXML:从 官方网站 下载头文件(rapidxml.hpp、rapidx…