A Look at Ruby Debuggers
There is one misconception about Ruby that's widespread both in and outside the Ruby community. The misconception is: Ruby does not have debuggers. Some argue that this is a problem of Ruby. Others try to interpret this perceived lack of debugging tools as wisdom and good style.
But no matter of the point of view: it's still a misconception. Ruby does indeed have debugging tools - actually quite a lot. Let's have a look at the available tools, both debugging GUIs, debugger implementations and debugging support in the various Ruby implementations.
What's a debugger?First off, let's clarify what a 'debugger' actually involves.
Debugging GUIs and interfacesOf course, the most important part of an interactive debugger - for the user at least - is the user interface. Command line interfaces for Ruby debuggers are available, such as the one shipped with the Ruby standard library, the Rubinius debugger. While these certainly allow to debug code, they also make it quite tedious to set breakpoints or see what's actually going on.
IDEs, while sometimes maligned in the Ruby space, certainly make debugging easier - after all, an IDE is an Integrated Development Environment. The Integrated part becomes important with debugging, as an IDE can integrate both code editing and debugging tools. Managing breakpoints can be done directly in the source editor - compared to taking the line number of a piece of code, going over to the command line debugger, and manually entering a breakpoint there. Features such as line-based stepping also become more useful with an IDE that opens the file of the current stack frame at the correct line.
IDEs with embedded scripting support also allow to script the debugger. An option, for instance, is Eclipse's EclipseMonkey extension which allows scripts written in JRuby. As these scripts run inside the same JVM as the Eclipse IDE, it's possible to access the Debugger instances and control it.
Debugger protocol or connecting to the backendA simple way to connect a debugger user interface like an IDE with the debugger backend is to use the command line interface and control that via stdin/stdout/stderr streams. With this, an editor's or IDE's debugger support can control the debugger, but at the same time make it easy for the user to manage breakpoints.
Another option is a wire protocol that allows to connect to the debugger via some mode of InterProcess Communication (IPC), nowadays often via TCP/IP. Network based protocols also allow the GUI and the debugger to be on different machines, i.e. debugging a remote machine using a local user interface..
Simple, text based, or at least documented, debugging protocols also allow to script the debugging process in any language. As a matter of fact, it's as easy as opening
telnetand connecting to the Ruby debugger. The protocol at debug-commons and DBGp commands consist of single line strings and responses in XML.
VM support or debugging backendTo allow for features such as breakpoints to work, the language runtime must at least provide minimal support to monitor and control execution. This can be as minimal as Ruby's tracing functionality: before a line of Ruby code is executed, Ruby invokes a callback function that is set with a call to
set_trace_func. The arguments passed to this function include information about the environment of the line of code Ruby is about to execute, such as it's line number, the name of the file it's in, it's class, etc. This is enough to implement breakpoint functionality: file name and line number allow to check in a list of registered breakpoints if there is one registered for the line.
If a breakpoint is hit, execution is suspended simply by not returning from the callback - the Ruby runtime can only continue once the callback returned. Building on this, features such as stepping and others can be implemented.
While it's possible to build debuggers using the tracing functionality, it's quite slow as every line executed comes with the overhead of the tracing callback that is executed before it is. The ideal solution would be to only incur cost for a breakpoint when the line of this breakpoint is actually executed. A runtime can implement this by modifying the loaded code - be it an AST or opcodes - at the location of the line of the breakpoint. Some language runtimes feature built-in debugging support, which integrates with it's execution mechanisms. Both Java and .NET binaries can ship with debugging information (i.e. mappings from files and line numbers to locations in the bytecode), which the built-in debugging support then uses to implement the debugger functionality. In the Java space, for instance, JVMs come with JVM Tool Interface (JVM TI) to access this functionality and the Java Debug Wire Protocol (JDWP) for connecting to the JVM.
Another approach is employed by the Rubinius debugger which uses accessible and modifiable opcodes of the Ruby code (Rubinius compiles Ruby source to opcodes prior to execution). A breakpoint is set by exchanging a regular opcode with a special opcode that suspends the current thread and notifies the higher layers of the debugger stack.
By making a lot of the infrastructure and management data structures accessible to the language, the language itself can be used to build the debugging mechanisms.
Debuggers and IDE support by Ruby implementationWith the basics set up, let's look at what's available, starting with Matz' Ruby Implementation (MRI), the most used and supported Ruby implementation. After that we'll look at the situation in JRuby, Rubinius and IronRuby - the tool support for these Ruby implementations and where they differ from MRI, either with tool support or performance.
Debugging backendsRuby 1.8.x, or MRI is the official Ruby interpreter, written in C. A host of debugging options are available for this version of Ruby. The tracing debugger is available, with its Ruby version shipped with the standard library. There are, however, faster implementations available. One is ruby-debug, which is implemented using a native extension.
Another option is shipped with the SapphireSteel's Ruby in Steel IDE: the Cylon debugger. It is also built using native code to implement the functionality, complete with using Ruby hooks to get notified about events such as method invocations etc. According to SapphireSteel's benchmarks, the Cylon debugger is significantly faster than the debugger written in Ruby and faster than ruby-debug.
GUIMany Ruby IDEs provide debugging support. The Eclipse based RDT (now part of Aptana and RadRails) has had debugging support for a long time, first connecting to the Ruby-based tracing debugger, later on with support for ruby-debug. RDT's debugging protocol has been factored out into the debug-commons project used in Netbeans Ruby to provide debugging. Another old timer in the Ruby IDE space is ActiveState's Komodo, which is built on the DBGp protocol to connect to it's protocol. Eclipse DLTK Ruby, which is also the base for CodeGear 3dRail, is another IDE leveraging Eclipse's debugger GUI. DLTK also uses DBGp to connect to backends. SapphireSteel's Ruby in Steel includes a debugger GUI, which allows fast debugging using their Cylon debugger.
The specific feature sets of the IDEs might vary, but they at least offer setting of breakpoints, stepping through code and variable views. Note: while IntelliJ offers Ruby editing support in their IDE, the IntelliJ Ruby roadmand lists debugging support as a future project.
Debugging backendsThe regular tracing based Ruby debugger works in JRuby as well. Additionally, a faster version is available as jruby-debug (also hosted at the debug-commons project), which uses Java as implementation language instead of Ruby, to reduce the overhead per executed line.
A new JRuby debugging backend comes from SapphireSteel. The company was already mentioned with their custom, fast Cylon debugger for MRI. Unlike jruby-debug, SapphireSteel's solution uses both Java and native code (via JNI) for the debugger backend.
GUIRuby IDEs that support
set_trace_funcdebugging work with JRuby as well, Netbeans and Apatana also ship with jruby-debug support . Cross language debugging support is obviously an added benefit for everyone who uses JRuby not just as a plain Ruby runtime, but to script Java classes with Ruby. In this case, cross language debugging is useful - i.e. showing both Ruby and Java stacks and variables when Ruby code calls into Java code.
The SapphireSteel IDE uses their own implementation of both backend and communication protocol, not based on any of ruby-debug or jruby-debug, which means it's tied to the Ruby in Steel IDE.
Debugging backendsRubinius has certainly come a long way - particularly in the past months, when it's debugging support jumped from non-existent to the front of the Ruby field in terms of debugging performance. The Full Speed Ruby Debugger allows to run a Ruby program with debugging enabled without the kind of performance overhead of other solutions, as explained above or in the linked news item.
Debugging benefits from some of Rubinius' design decisions, which makes a lot of the VM infrastructure and metadata available to regular Ruby code at runtime. Opcodes or the ParseTree of the loaded Ruby code, as well as stacktraces are accessible. The introspection capabilities go further, e.g. with SendSites. SendSites represent the site of a message send ("method invocation"), and can be linked to the method. This allows to figure out the configuration of the loaded code at runtime, but also helps as a profiling or code coverage tool. Every message send increments a counter of the SendSite; as this information is available to regular Ruby code, writing either a simple profiler or at least a code coverage tool is a matter of a few lines of code.
GUICurrently the user interface to the Rubinius debugger is a command line interface that allows to manage breakpoints, stepping but also looking at the opcodes of running Ruby code or their source files. A useful command is
sexp [method], which returns ParseTree s-expr representation of the AST of
[method]. (Omitting the argument shows the AST of the current method as ParseTree s-expr). This is quite useful information, particularly for code using metaprogramming - code generated at runtime obviously has no source code. Being able to look at the generated, loaded code can help with debugging the code that does the metaprogramming. Looking at the s-expr is a step up from trying to guess what a generated piece of code does - it gets more convenient by falling back to ParseTree-based tools such as Ruby2Ruby, which takes the s-expr and formats it back to Ruby source code.
Rubinius' connection to debugger GUIs - at the time of publication of this article - does not exist. However, this is about to change very soon as implementations for the debugging protocols are worked on right now. Judging from the speed with which the debugging support was implemented, debugger GUI support is not far off (the debugging protocol implementation is the easy part of debugger implementation). Once either the debug-commons or DBGp protocol are supported, IDEs using these protocols will be able to make use of Rubinius.
IronRubyIronRuby targets the .NET platform by generating MS IL code. It makes use of the DLR, a system that makes collects common functionality for language implementations, such as MS IL generation from expression trees and more..
Debugging backendsThe DLR generates .NET MS IL and can also generate the MS IL debugging information, which means IronRuby can make use of both the .NET debugging facilities as well as the Visual Studio debugger GUI.
GUIVisual Studio can be used, but Ruby's SapphireSteel Ruby in Steel IDE - also based on Visual Studio - has support for IronRuby development. Debugging features are supposed to be available in a future version.
The rest of the fieldThis article showed a sampling of the currently available debugging tools for Ruby without a claim of completeness. There are other GUIs and backends available in IDEs such as ActiveState's Komodo or backends with varying levels of support for Ruby implementations or debugging features. The Ruby implementation XRuby was not mentioned, although it has debugging support. Ruby 1.9 is also not mentioned, as it still seems to be under heavy development, although it has been officially released. As Ruby 1.9's VM also uses a bytecode interpreter, a similar scheme as Rubinius' might be possible.
At last, a disclaimer: The development of the alternative Ruby implementations and debugging support is moving at a brisk pace right now. So: take this article as an overview of the debugging support in Ruby - by the time you read this, the actual debugging support of Ruby implementations and tools is likely to have changed and improved.
Srini Penchikala Aug 21, 2014