JNI debugging – extreme way, or what your iPad mini and ssh sessions can do for you

Yes! This article is about how to debug code using iPad mini.

This note was added after publishing the post.
I was strongly against iPads as being anything else apart advanced Angry Birds playing machine. Well, maybe e-mail reader and web browsing machine as well. But that’s it. Nothing more should be possible there – that’s what I thought. Recently, I had half day of totally spare time. Nothing to do, poor network connection. Disaster!! However, I had my iPad with me, and I had one thing circulating in my head. How to debug JNI code efficiently with ssh. Eventually, it turned out that I can do everything using 9″ screen and bluetooth keyboard. Read on.

This post was prepared entirelly using iPad and ssh sessions (No cheating! I promise). Yeap, this is possible to do actual dvelopment using iPad. Below, you can find the list of software/hardware used for this purpose:

software:

– WriteRoom – for creating post content
– Prompt – for accessing remote machine
– Safari – for posting the content into WordPress
– tmux – for better living with ssh via Prompt
– screen – for preventing loosing ssh session interrupted by Prompt
– vi – well, do you have anything better here?
– jdb – debugging Java with good old CLI
– gdb – debugging jdb with good old CLI
– java – for compiling and running Java code

hardware:

– iPad mini (without retina :) )
– Apple’s aluminium bluetooth keyboard (you can survive with on-screen keyboard, however – this was the only “cheat” from my side)

task:

– debug Java code that uses JNI
– debug native code called via JNI

solution:

– start Java VM i debug mode and listening on a given port
– start jdb and connect to JVM
– start gdb and connect to JVM

example code:

Example code is a very simple Java application that calls native procedure written in C. I will not go into details here when it comes to JNI. All you get here is a simple JNI code that will be used during the session. So, let’s go into details.

First, create some directory where you can store all the codes and then create following files inside the directory

/* Simple.h */
#include "jni.h"
JNIEXPORT void JNICALL Java_Simple_displayMessage(JNIEnv *env, jobject obj);
/* Simple.c */
#include 
#include "Simple.h"
JNIEXPORT void JNICALL Java_Simple_displayMessage(JNIEnv *env, jobject obj) {
  printf("Hello from JNI!\n");
}
/* Simple.java */
class Simple {
  public native void displayMessage();
  static {
    System.loadLibrary("Simple");
  }
}
/* Main.java */
public class Main {
  public static void main(String[] args) {
    System.out.println("library: " + System.getProperty("java.library.path"));
    Simple simple = new Simple();
    simple.displayMessage();
  }
}

After files are ready, you can compile and execute the whole scenario using following command:

export JAVA_HOME=/wherever_your_java_path_is/
$JAVA_HOME/bin/javac -g *.java
$JAVA_HOME/bin/javah -jni Simple
cc -g -shared -fpic -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux \
  Simple.c -o libSimple.so
export LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH
$JAVA_HOME/bin/java Main

After executing the code, you should see message

"Hello from JNI!"

This is the result of native code. Now, what we will do here is going into native code using gdb and at the same time debugging the Java code from jdb.

First, let’s start JVM process and susspend it – we will wait for the connection from the jdb. This way, application will be in pending state and Main.class will not be executed – yet. To do so, call java following way

java -agentlib:jdwp=transport=dt_socket,\
server=y,suspend=y,address=32887 \
-Djava.library.path=.:"$LD_LIBRARY_PATH" \
-cp . Main

You will see following message

Listening for transport dt_socket at address: 32887

And that’s how will terminal look like

image

Note the value “32887” – we will need it. This is the port number we will connect through from the jdb to JVM running Main.class.

Now, go to the second terminal and start jdb. If you want to have access to sources, use -sourcepath argument. Basically, all you have to do is to call

jdb -sourcepath . -attach 32887

Afte calling it, you will see jdb being attached to the JVM

shell>jdb -sourcepath . -attach 32887
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
Initializing jdb ...
> 
VM Started: No frames on the current call stack

main[1] 

You should see something like this

image

And now, we are ready to debuging. First, let’s make sure that we will brake in the code. Inside jdb type in:

stop in Main.main
run

After we hit the breakpoint, list the code using “list” command. If you take a look at the Simple.java code, you can see that during creation of the class static part of the class will load the library (native code). Let’s break after this line. We can do this following way

stop at Main:5
continue

What you should see will look following way

shell>jdb -sourcepath . -attach 32887                                     
shell>jdb -sourcepath . -attach 32887
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
Initializing jdb ...
> 
VM Started: No frames on the current call stack

main[1] stop in Main.main
Deferring breakpoint Main.main.
It will be set after the class is loaded.
main[1] run
> Set deferred breakpoint Main.main

Breakpoint hit: "thread=main", Main.main(), line=4 bci=0
4        Simple simple = new Simple();

main[1] list
1    /* Main.java */
2    public class Main {
3      public static void main(String[] args) {
4 =>     Simple simple = new Simple();
5        simple.displayMessage();
6      }
7    }
8    
main[1] stop at Main:5
Set breakpoint Main:5
main[1] cont
> 
Breakpoint hit: "thread=main", Main.main(), line=5 bci=8
5        simple.displayMessage();

main[1] 

Inside Prompt you will something like this:

image

At this point we are just before calling the native code. This is the right time to unleash gdb. In yet another terminal window we will look for executed JVM and we will attach to the correct one

shell>jdb -sourcepath . -attach 32887                                     
shell>jps
14586 TTY
14531 Main
15161 Jps
shell>gdb /proc/14531/exe 14531
...
...
(gdb)

Now, we can take a look whether library is already loaded

info shared

You should see the entry that contains library that we use:

...
...
0x00002b8ba2864540  0x00002b8ba2864678  Yes         
/pfs/home/michalo/jni 
/libSimple.so
(*): Shared library is missing debugging information.
(gdb) 

So, let’s break in the native code

(gdb) info shared
From                To                  Syms Read   Shared Object Library
...
...
...
 0x00002b9b18000540  0x00002b9b18000678  Yes         /pfs/home/michalo/jni 
/libSimple.so
(*): Shared library is missing debugging information.
(gdb) break Java_Simple_displayMessage
Breakpoint 1 at 0x2b9b18000624: file Simple.c, line 5.
(gdb) cont
Continuing.

At this point we are contiuing the JVM’s code. Let’s get back to jdb and continue as well. As you will see, nothing happens. The point here is that we have hit the breakpoint inside gdb

[Switching to Thread 0x2b9b08e86700 (LWP 16022)]

Breakpoint 1, Java_Simple_displayMessage (env=0x401181d0, 
    obj=0x2b9b08e85978) at Simple.c:5
5         printf("Hello from JNI!\n");
(gdb) list
1       /* Simple.c */
2       #include 
3       #include "Simple.h"
4       JNIEXPORT void JNICALL Java_Simple_displayMessage(JNIEnv *env, job
ject obj) {
5         printf("Hello from JNI!\n");
6       }
7
(gdb) 
[1] 0:java

image

And now, we can debug native code. When we execute continue inside gdb, jdb will continue as well, and the application will eventually finish.

shell>jdb -sourcepath . -attach 32887                                     
shell>java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=3 
2887 -Djava.library.path=.:"$LD_LIBRARY_PATH" -cp . Main
Listening for transport dt_socket at address: 32887
Hello from JNI!
shell>

And that’s pretty much it. So, just to conclude. In order to debug JNI and JVM at the same time:

– start JVM in debug mode (listening for the JPA connections)
– start jdb and debug the Java code
– start gdb and attach to JVM in order to get into native code

And for the summary related to the above scenario, here are few remarks on the feelings related to the whole proceess:

– I miss Cmd-Tab, Cmd-` on the iPad. Clicking Home button is so frustrating (especially when you use external keyboard)
– Prompt is dropping connections from time to time. This might be frustrating if you lose part of the work. Use screen to prevent loosing your nerves.
– Prompt will missbehave with selecting multiple lines. It simply gets lost from time to time and created arbitrary text selections when you play with the range of selected text
– Prompt will not provide you with the shell history :(
– Prompt will produce occasionally some artifacts on the screen (e.g. text from the other tmux session – which is strange)
– WriteRoom has this strange issue that attaching external keyboard leaves a bar with some functional keys and you will not see your text as it goes beyound this bar. To avoid this issue, go to projects, and choose full-screen mode (right bottom corner)
– Mouse – maybe it will be possible at some point to use mouse on iPad. Selecting text areas with touching the screen is a nightmare. Especially, when you are on external keyboard.

Anyway, I must admit that we got to the point where iPad based developement is possible and perfectly doable. Next time, I will try to do the same with Nexus 7 and compare my experience. But still, it took much more time to prepare this text comparing to what I would have to spent using OS X. And, I still have no idea how to put images into post :)

In fact, this part turned out to be super easy. WordPress simply allows you to pick images from the camera roll. And with iOS 7 it is just a matter of few clicks to make a simple image processing

Note, you can avoid using three separate terminal sessions by using gdb with java by calling it following way

export LD_LIBRARY_PATH=.:"$LD_LIBRARY_PATH"
gdb java
set args -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=32887\
 -cp . Main
run

This way, you will be debugging JVM the same way as before.

If you are looking for more JNI samples, take a look here: http://jnicookbook.owsiak.org