RubyMotion Debugging Guide for Android

This article covers how to debug RubyMotion Android projects using the builtin debugging facility. RubyMotion apps can be debugged either on the emulator or the device.

At the time of this writing, the debugging experience in RubyMotion is still a work in progress, and this document might change any time to reflect the progresses that have been made in this regard.

1. Getting Started

The RubyMotion debugger for Android projects is based on GDB, the Android NDK debugger.

GDB is traditionally used to debug programs written in C-based languages, however, RubyMotion brings Ruby support to GDB, allowing it to connect and introspect RubyMotion processes.

Note
The GDB support is at this point experimental and also quite low-level. Our goal is to build a higher-level, friendlier debugger on top of GDB that will provide a better experience for Ruby developers.

This document aims at covering the main features that one might need in order to debug a RubyMotion app with GDB. This document is not a complete GDB manual. We highly recommend reading the official user manual if an exhaustive guide is needed.

Note
In this guide we will use the longhand versions of all debugger commands, but most, if not all, have shorthand versions which you can find in the official user manual.

1.1. Debugging Symbols

The RubyMotion compiler implements the DWARF debugging format metadata for the Ruby language. This allows external programs such as debuggers or profilers to retrieve source-level information about an existing RubyMotion application.

In Android, the metadata is saved right inside the application ELF shared library, in the build directory of your project.

Both development and release modes have debugging symbols, however, as the release mode activates compilation optimizations, the debugging experience will be better under the development mode. For example, in the release mode, certain local variables and arguments might not be accessible in the debugger as they could be optimized.

1.2. Attaching the Debugger

In order to start the debugger, the debug option can be set to any value on the appropriate rake target.

When working with the emulator rake task, the debugger will directly attach itself to the app and replace the interactive shell (REPL).

$ rake emulator debug=1

When working with the device rake task, the build system will start the iOS debugging server on the device then remotely attach the debugger on your shell right after the application has been deployed on the device.

$ rake device debug=1

2. Managing Breakpoints

To set a breakpoint to a given location in the source code, use the break command and pass both the file and the line number, separated by :.

As an example, the following command sets a breakpoint on the 16th line of the hello_view.rb file.

(gdb) break main_activity.rb:16
Breakpoint 1 at 0x5ec294d0: file main_activity.rb, line 16.

The info breakpoints command can be used to list the breakpoints that have been set in the current debugger environment.

(gdb) info breakpoints
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x5ec294d0 in rb_scope__dispatchTouchEvent__ at main_activity.rb:16

As you can see our breakpoint main_activity.rb:16 is right there and is enabled. The enable and disable commands can respectively enable or disable a given breakpoint using its number.

Since our breakpoint is number 1 in the list, we can disable it like this:

(lldb) disable 1

3. Getting the Backtrace

Once you hit a breakpoint, it is often interesting to check out the execution backtrace, which will tell you where the method is called from.

This can be done by using the backtrace command.

(gdb) backtrace
#0  rb_scope__dispatchTouchEvent__ (_self=0x53e00025, event=0x53e0fe29)
    at main_activity.rb:16
#1  0x5ec29e10 in __unnamed_3070 ()
   from /Users/lrz/src/RubyMotionSamples/android/Hello/build/Development-19/lib/armeabi/libpayload.so
#2  0x4164c350 in ?? ()
#3  0x4164c350 in ?? ()

Backtrace frames in your code can be identified with the rb_scope__ prefix and the file:line information.

3.1. Frames

Here, the very first frame in the backtrace is the method defined in the breakpoint location: dispatchTouchEvent. The other frames below the breakpoint are native Android NDK calls.

The frame command lets you switch to a specific frame in the backtrace. By default you will be at the top frame (#0), but assuming you want to go down to frame #2, in order to inspect its context, you can type the following command to do so.

(gdb) frame select 2

Obviously it mainly matters when you want to go down to a specific Ruby-defined location in the backtrace.

3.2. Threads

The backtrace command only returns the backtrace of the current thread. When dealing with a multithreaded program, you may sometimes want to print the backtrace of all running threads, for instance when you are debugging a race condition.

The following command will print the backtrace of all the running threads in the terminal.

(gdb) thread apply all backtrace

Similar to switching frames, the debugger will let you switch threads using the thread command. This can be useful if you want to inspect a specific Ruby method frame in another running thread. The following command will switch the debugger prompt to the thread #4.

(gdb) thread 4
[Switching to thread 4 (Thread 10224)]
#0  0x401434b0 in recvmsg ()
   from /Users/lrz/src/RubyMotionSamples/android/Hello/build/Development-19/obj/local/armeabi/libc.so

4. Inspecting Objects

After checking the backtrace, you may want to inspect the objects around. The debugger will let you print them using specialized commands.

4.1. Local Variables

We just hit a breakpoint defined in the onCreate method. As you can see from the breakpoint, we are inside a function that accepts two arguments: _self and savedInstanceState.

In RubyMotion, the _self argument is a pointer to the self object exposed in Ruby, which represents a reference to the receiver of the method. In the debugger, _self is visible as the first argument of the method.

We can inspect the values of both _self and savedInstanceState by using the print-ruby-object command. This RubyMotion-defined command sends the inspect message to the given object and returns its value. The command can also be called using the pro shortcut which we will use as a convenience.

Breakpoint 1, rb_scope__onCreate__ (_self=0xc3e00019, savedInstanceState=0x67c0001d)
    at main_activity.rb:13
13	  end
(gdb) pro _self
#<MainActivity:0xc3e00019>
(gdb) pro savedInstanceState
#<android.os.Bundle:0x67c0001d>

The list of local variables can be printed using the info locals command. The list will also include the addresses of each local variable.

5	    elems = []
(gdb) info locals
elems = 0x5d800025

These local variables can also be individually inspected on the terminal by using the pro command.

(gdb) pro elems
[]

4.2. Instance Variables

Instance variables of an object can be printed using the print-ruby-ivars command.

(gdb) print-ruby-ivars
@text = #<android.widget.TextView:0x1ef00059>

5. Control Flow

The next command will continue the execution of the program until the next source-level location. This is usually the very next line in the Ruby source code. This means that the debugger has not yet executed the line that it indicates is the current line, keep this in mind when inspecting variables and their values.

Breakpoint 1, rb_scope__onCreate__ (_self=<optimized out>, savedInstanceState=<optimized out>)
    at main_activity.rb:5
5	    elems = []
(gdb) n
6	    elems << 1
(gdb) pro elems
[]
(gdb) n
7	    elems << 2
(gdb) pro elems
[1]

The continue command will continue the execution of the program until it reaches a breakpoint.

(gdb) continue
Continuing.

When the program runs, you can always stop its execution and go back to the debugger prompt by typing the control`c+ (^C`) keyboard shortcut.

^C
Program received signal SIGINT, Interrupt.
[...]
(gdb)

If you want to quit the debugger, just type the quit command and confirm that you want to exit. It will terminate the application and return you back to the shell prompt.

(gdb) quit
A debugging session is active.

	Inferior 1 [Remote target] will be detached.

Quit anyway? (y or n)
y
$