查看更多

去年deepin社区发布了自己的IDE:deepin-IDE,当时得到了众多开源社区用户的广泛关注,本文试着将 deepin-IDE “调试”部分的一些实现方法与大家进行分享。

deepin-IDE 的调试功能是选用 DAP(Debug Adapter Protocol )调试适配协议实现的,整体架构是围绕该协议搭建。

什么是 DAP 协议

DAP 即调试适配协议( Debug Adapter Protocol ),顾名思义,它是用来对多种调试器进行抽象统一的适配层,将原有 IDE 和调试工具直接交互的模式更改为和 DAP 进行交互。该模式可以让 IDE 集成多种调试器变得更简单,且灵活性更好。

在 IDE 中的调试功能有许多小功能组成,包括单步执行、断点、查看变量值等,常规的实现方式是在每个 IDE 中去实现这些逻辑,且因为调试工具的接口不同,还需要为每个调试工具做一些适配工作,这将导致大量且重复的工作,如下图所示:
图片

调试适配器协议背后的想法是标准化一个抽象协议,用于开发工具如何与具体调试器通信。这个思想和 LSP(Language Server Protocol)和 BSP(Build Server Protocol)类似,都是通过协议去统一相同功能在不同工具之间的差异性。其所处位置如下图所示,其中左边为不同的开发工具,右边为不能同的调试器,不同于开发工具和调试器直接交互的方式,DAP 将这些交互统一了起来,让开发工具和调试工具都面向 DAP 编程。

上图中的交互是通过协议进行,所以不会像通过 API 的方式存在语言限制,可以更好的适应调试器的集成。

DAP 如何工作

以下部分解释了开发工具(例如 IDE 或编辑器)和调试适配器之间的交互,包括具体的协议格式说明、交互流程等。

调试会话

开发工具有两种基础的方式和调试器进行交互,分别是:
【单会话模式】
在这种模式下,开发工具启动一个调试适配器作为一个单独的进程并且通过标准的std接口进行通信。在调试会话的结束时调试适配器就终止,对于当前的调试会话,开发工具往往需要实现多个调试适配。

【多会话模式】
在这种模式下,开发工具不会启动调试适配器,而是假定它已经在运行并且会在特定端口上侦听连接尝试,对于每个调试会话,开发工具在特定端口上启动一个新的通信会话并在会话结束时断开连接。

在与调试适配器建立连接后,开发工具和调试适配器之间通过基础协议进行通信。

基础协议

基础协议由两部分组成,包括头和内容(类似于 HTTP),头部和内容部分通过“\r\n”进行分割:
【协议头】
协议头部分由字段组成, 每个头字段由一个键和一个值组成,用‘:’(一个冒号和一个空格)分隔, 每个头字段都以“\r\n“结尾。由于最后一个协议头字段和整个协议头本身都以 \r\n 终止,并且由于协议头是强制性的,所以消息的内容部分总是在(并唯一标识)两个 \r\n 序列之前。当前只支持一个协议头字段:

头字段名值类型描述
Content-Length数字这个字段是必须的,用来记录内容字段的长度,单位是字节。

协议头部分使用的是“ASCII”编码。

【内容部分】
内容部分包含了实际要传输的数据,这些数据用 JSON 格式来描述请求、响应和事件。内容部分用的是 utf-8 编码

为了有个具体的认识,这里举个简单的例子。在调试过程中,开发人员经常会使用到下一步操作,在 DAP 中其协议为:

Content-Length: 119\r\n
\r\n
{
    "seq": 153,
    "type": "request",
    "command": "next",
    "arguments": {
        "threadId": 3
    }
}

类型是“请求”,命令是下一步,参数部分可以携带多个,这里是用的线程Id。 这个协议看着挺简单的,是吧?接下来就讲讲如何使用它。

使用方法

详细的使用方法这里就不涉及,因为用一个时序图就可以说明:
图片
可以看到,初始化、请求、响应等必要的步骤都在图中。其中调试适配器可以理解为调试器的抽象,调试功能的最终执行者是由对应语言的调试工具实现的。

在 deepin-IDE 中的实现

在 deepin-IDE 中,调试功能的实现是结合 cppdap + debugmanager 实现的。

cppdap 是一款基于 C++ 开发的 SDK,基本实现了 DAP 的全量协议。 deepin-IDE 的客户端和服务端都是应用的该 SDK 进行开发,据此可以实现以下功能:

  1. 通信功能,包括服务端的 TCP 监听,客户端的 TCP 连接等;
  2. DAP 协议的封装,并实现协议的串行化和解串行化;
  3. 提供注册回调功能,从而可以在回调内处理各种事件、请求等;
    它的层级结构如下:
    图片

用 cppdap 可以减少客户端和服务端不少工作量,也统一了两边的协议数据。而 debugmanager 可以理解为调试器的抽象,包含所有必要的调试要素。整体结构如下:
图片
左边是客户端,右边是服务端,内部实现如下:

客户端实现
客户端包含了两个个主要功能,一个是和 DAP 服务端进行交互,发送调试命令或处理返回的数据;另一个是将DAP 数据转换后显示到用户界面,并响应界面发送的事件。概括起来就包含业务模块、事件模块、DAP 模块和界面4个部分。

  • 业务模块包含了插件类、调试参数、调试管理类等,其中插件类负责插件加载、初始化、获取上下文等,调试管理类用来组合事件、DAP、界面几个模块。 事件模块
  • 事件模块包含两个子模块,分别是事件发送和事件接收,比如页面跳转事件、添加\移除断点事件等。
  • DAP 模块基于 cppdap 开发,采用层级结构,底层是原始 DAP 协议封装,中间层是针对业务做的进一步封装,简化了向外提供的接口,最上层是对整个调试功能的整合,包括数据缓存、界面元素、命令收发。
    图片
  • 界面模块包含堆栈界面、变量界面、断点列表、异步对话框等,用于 DAP 的数据展示。
    图片
    如上图所示,灰色部分为 DAP 客户端的界面呈现。

服务端实现
服务端的功能分为两个部分,一个是基于 cppdap 实现命令的收发,另一个是与 gdb 交互,实现调试程序的启动、暂停、退出等一系列动作。

  • 和客户端一样,服务端也是基于cppdap实现的通信和协议封装和解析。 调试工具
  • 和调试工具的交互是通过进程调用的方式实现,接收进程输出得到返回信息。如果调试工具本身支持 DAP 协议,则可以直接交互。

附录/参考文档:

(1)DAP:https://microsoft.github.io/debug-adapter-protocol/overview
(2)deepin IDE 使用参考:https://wiki.deepin.org/zh/05_HOW-TO/02_%E5%BC%80%E5%8F%91%E7%9B%B8%E5%85%B3/deepin-unioncode
(3)https://distrowatch.com/table.php?distribution=deepin


Veronicaaa
4 声望2 粉丝