Ruby 调试器一览

阅读数:5627 2008 年 5 月 4 日

有一个关于 Ruby 的误解在 Ruby 社区内外广泛流传,即:Ruby 没有调试器。有些人说这是 Ruby 的一个问题。其他人则试图将所谓的缺少调试工具解释为智慧之举和良好风格。这些观点都是误解。Ruby 明明是有调试工具的——实际上有很多。让我们来看一看这些现有的工具,包括调试GUI、调试器实现和各种Ruby 实现中的调试支持。

什么是调试器?

首先,让我们搞清楚“调试器”实际上涉及了哪些东西?

调试的 GUI 和接口

当然了,交互式调试器最重要的部分——至少对于用户来说——是用户接口。用户可以使用 Ruby 调试器的命令行接口,例如和 Ruby 标准库一起提供的 Rubinius 调试器。它显然可以用来调试代码,只不过设置断点或查看运行状态会比较麻烦。

IDE 虽然有时在 Ruby 世界中不太受推崇,但它无疑令调试变得更简单了——毕竟,IDE 就是集成开发环境。集成对于调试来说很重要,而 IDE 正是把代码编辑和调试工具整合在一起了。你可以在源代码编辑器中直接管理断点——而不用记下代码的行号,进入命令行调试器中,然后手工设置断点。在 IDE 中,诸如基于行的单步调试之类的功能也更加实用,可以正确的找到所打开的文件的栈结构和所在行。

带有嵌入式脚本支持的 IDE 还允许对脚本进行调试。例如 ,Eclipse 的 EclipseMonkey 扩展支持用 JRuby 写成的脚本。由于这些脚本和 Eclipse IDE 都运行在同一个 JVM 上,由此调试器实例便可以被访问和控制了。

调试器协议还是连接到后端

把像 IDE 这样的调试器用户接口和调试器后端连接起来的一个简单方法是:使用命令行接口,并通过标准的 stdin/stdout/stderr 流来进行控制。这样,编辑器或者 IDE 的调试器支持就可以控制调试器,同时也让用户管理断点变得更加方便了。

另外一个方法是采用线路(wire)协议,它允许通过某种模式的进程通讯(IPC),现在一般是通过 TCP/IP 来连接到调试器。基于网络的协议还允许 GUI 和调试器分布在不同的机器上,也就是说可以使用本地的用户接口来对远程机器进行调试。

基于文本的或者至少基于文档的简单调试协议也允许使用任何语言来编写调试进程脚本。实际上,连接到 Ruby 调试器和打开 telnet 一样简单。 debug-commons DBGp 命令的协议就是由单行字符串和 XML 应答构成的。

VM 支持还是调试后端

为了支持断点等功能,语言运行时至少得提供监视和控制执行的支持。可以简单地像 Ruby 的跟踪(tracing)功能一样:在一行 Ruby 代码执行之前,Ruby 会调用一个叫做 set_trace_func 的回调函数。传过去的参数包括即将执行的那行代码的环境信息,比如行号,所属文件的名字和所属的类等等。这些信息就足以实现断点功能了:在一个断点注册表里面检查文件名和行号,看看是否被注册了。

当遇到一个断点时,执行就被挂起,只要不从回调中返回即可——Ruby 运行时只能在回调返回后才能继续运行。基于这些,就可以实现单步调试等功能了。

虽然使用跟踪功能可以实现一个调试器,但是在执行每一行之前都要先执行跟踪回调,显然太慢了。理想地解决方案是仅在执行有断点的行时才引发断点处理。运行时可以通过修改已加载的代码来实现此功能——不论是 AST 还是操作码(opcodes)——在有断点的行上。有些语言的运行时提供了内建的调试支持,与执行机制整合在一起。Java 和.NET 的二进制代码都提供调试信息(即从文件和行到字节代码位置一个映射),让内建的调试支持能使用这些信息来进行调试。在 Java 世界中,例如,JVM 配合 JVM 工具接口(JVM TI)一起实现了这个功能以及用来连接到 JVM 的 Java 调试线路协议(JDWP)。

还有一个方法是 Rubinius 调试器所使用的,它使用可访问和可修改的 Ruby 代码中的操作码(Rubinius 把 Ruby 源代码先编译成操作码然后再执行)。

通过把一个一般操作码替换成一个特殊操作码来设置一个断点,而这个特殊操作码则用来挂起当前进程并通知调试堆栈中的高层。 通过设置大量的基础体系和管理数据结构以供语言来访问,语言本身就可以用来建立调试机制。

各种 Ruby 实现的调试器和 IDE 支持

有了以上基础,再让我们来看一看现有的调试器。从用得最广、支持得也最多的 Matz 的 Ruby 实现(MRI)开始。之后让我们再看一看 JRuby、Rubinius 以及 IronRuby 的现状——看看这些 Ruby 实现的工具支持,还有它们与 MRI 以及其工具支持和性能的区别。

Ruby/MRI

调试后端

Ruby 1.8.x,也就是 MRI,是官方的 Ruby 解释器,是用 C 语言实现的。我们最常见的调试器就是针对它的。这个跟踪调试器是配合它的 Ruby 版本以及标准库一起使用的。另外还有更快的实现。比如 ruby-debug ,它是使用本地扩展来实现的。

还有一个选择是随 SapphireSteel 的 Ruby in Steel IDE 提供的: Cylon debugger 。它也是通过本地代码来实现功能,使用 Ruby 钩子来获得诸如方法调用等事件通知而完成的。 SapphireSteel 的标准测试表明,Cylon 调试器比用 Ruby 写的调试器快得多,也比 ruby-debug 要快。

GUI

许多 Ruby IDE 提供都调试功能。基于 Eclipse 的 RDT (现在是 Aptana 和 RadRails 的一部分)在很久以前就开始提供调试支持了,一开始是连接到基于 Ruby 的跟踪调试器上,后来转而支持 ruby-debug。RDT 的调试协议被分解到了debug-commons 项目中,该项目用于Netbeans Ruby,以提供调试功能。在Ruby IDE 世界中还有一个古老的 ActiveState's Komodo ,它是基于 DBGp 协议的。另外一个能与 Eclipse 的调试器 GUI 抗衡的 IDE 是 Eclipse DLTK Ruby ,它也是 CodeGear 3dRail 的基础。DLTK 也使用 DBGp 来连接到后端。SapphireSteel 的 Ruby in Steel 包含了一个调试器 GUI,它允许使用 Cylon 调试器来进行快速调试。

这些 IDE 的功能虽不尽相同,但至少都提供了断点、单步调试和变量查看功能。 注意:尽管 IntelliJ 在它们的 IDE 中提供了编辑 Ruby 功能,但在 IntelliJ Ruby 的蓝图中调试支持是作为一个未来项目的

JRuby

调试后端

基于跟踪的常规 Ruby 调试器也能用于 JRuby。除此之外,更快的版本是 jruby-debug (也属于 debug-commons 项目),它是用 Java 而不是 Ruby 语言来实现的,从而减少了每行的执行开销。

还有一个新的来自SapphireSteel 的JRuby 调试后端。刚才提到了这个公司,他们还做了MRI 的快速Cylon 调试器。和jruby-debug 不同,SapphireSteel 的解决方案同时使用Java 和本地代码(通过JNI)实现了调试器后端。

GUI

支持 set_trace_func 调试的 Ruby IDE 也能用于 JRuby。另外 Netbeans 和 Apatana 也提供了 jruby-debug 支持。对于那些不止把 JRuby 当作普通 Ruby 运行时、还要用 Ruby 调用的 Java 类的人来说,显然很需要支持跨语言的调试。当 Ruby 核调用 Java 核时,最好同时显示 Ruby 和 Java 的堆栈和变量。

SapphireSteel IDE 使用他们自己实现的后端和通讯协议,而不是基于 ruby-debug 或者 jruby-debug,这意味着它是被绑定在 Ruby in Steel IDE 中的。

Rubinius

调试后端

毫无疑问, Rubinius 取得了长足的进步——特别是在过去的几个月中,它的调试支持从没有一跃成为 Ruby 界中的佼佼者(根据调试性能表现)。全速Ruby 调试器允许伴随调试运行一个Ruby 程序,而没有其他方案中的那种性能消耗,正如前面解释的或者链接新闻中所述的一般。

Rubinius 的设计决定了其调试功能的强大,使得在运行时常规的 Ruby 核可以使用大量的 VM 基础结构和原数据。操作码和已加载 Ruby 核的解析树(ParseTree),以及堆栈踪迹(stacktrace)都是可访问的。内部追查的能力更强了,例如使用SendSites。 SendSites 指出了消息传递到哪(“方法调用”),它还能链接到方法上。这样就可以获得在运行时中已加载代码的配置,但也起到了代码分析和覆盖工具的作用。每发一条信息,Sendsite 的计数器就会增加;由于这个信息也能用于 Ruby 代码,所以写一个简单的代码分析工具或者至少是代码覆盖工具就只是几行代码的事。

GUI

现在 Rubinius 调试器的用户接口还是命令行界面,它可以管理断点、单步调试,也能查看正在运行的 Ruby 核的操作码或者它们的源文件。 sexp [method]是一个实用的命令,它返回[method]的 AST 的 ParseTree 符号表达式(s-expr,忽略参数的情况下把当前方法的 AST 表示为 ParseTree 符号表达式)。这是十分有用的信息,特别是对于那些使用元编程(metaprogramming)的代码——运行时所生成的代码显然不含源代码。能够看到那些生成的被加载的代码显然对于调试那些元编程的代码有帮助。另外,能够看到符号表达式也比试图去猜测生成的代码是干什么的更进了一步,也更加方便了——通过采用基于 ParseTree 的工具,比如 Ruby2Ruby ,它是一个接收符号表达式并格式化后返回给 Ruby 源代码的工具。

直到本文发布之日为止,Rubinius 和调试器 GUI 的连接还不没有出现。不过,由于现在调试协议实现已经可以工作了,这个状况即将发生改变。从实现调试支持的速度来判断,对调试器 GUI 的支持也不远了(调试协议的实现是调试器实现中的一个简单部分)。一旦它支持了 debug-commons 或者 DBGp 协议,采用这些协议的 IDE 就能够用于 Rubinius 了。

IronRuby

IronRuby 生成的是 MS IL 代码,它目标是.NET 平台。它使用 DLR,这个系统收集各种语言的公共功能,比如表达式树等产生的 MS IL。

调试后端

DLR 生成 .NET MS IL,也生成 MS IL 调试信息。这意味着 IronRuby 既可以使用.NET 调试工具,也可以使用 Visual Studio 调试器的 GUI。

GUI

你可以使用 Visual Studio,而 Ruby 的 SapphireSteel Ruby in Steel IDE——也是基于 Visual Studio 的 IDE——也支持 IronRuby 开发。在以后的版本中肯定会增加调试功能。

其他问题

这篇文章介绍了一部分现有的 Ruby 调试工具,而不是全部。还有别的 IDE 的 GUI 和后端,比如 ActiveState 的 Komodo,还有一些不同程度地支持 Ruby 实现或调试功能的后端。这里没有提到 XRuby 这个 Ruby 实现,它也支持调试。同样也没有提到Ruby 1.9,尽管已经官方发行了,但它还在紧锣密鼓的开发中。由于Ruby 1.9 的VM 也使用字节码解释器,那么很可能会采用类似Rubinius 的方案。

最后是免责声明:现在,其他 Ruby 实现和调试支持正在飞快地开发着。所以,请把此文看作是对 Ruby 调试支持的概述——实际上,在你阅读时,各种 Ruby 实现的调试支持和工具很可能已经有了变化和改进。

查看原文: A Look at Ruby Debuggers

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论