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
$