Note: This tutorial assumes that you have completed the previous tutorials: android_ndk/Tutorials/How to cross-compile any ROS package.
(!) Please ask about problems and questions regarding this tutorial on answers.ros.org. Don't forget to include in your question the link to this page, the versions of your OS & ROS, and also add appropriate tags.

Wrapping your native code as a rosjava node

Description: Steps to create a rosjava android project and add your native compiled nodes as rosjava nodes.

Tutorial Level: ADVANCED

Next Tutorial: android_ndk/Tutorials/UsingPluginlib

Prerequisites

  1. Knowledge of Android development
  2. Have the following software installed in your system:
    1. ROS environment
    2. Android SDK
    3. Android NDK
    4. Completed the previous tutorial

So at this point you have successfully cross-compiled your library into device native code. A convenient way of running ROS nodes on Android devices is using ROSJAVA but in order to use the library in your Java application you need to create a “Java Native Interface” (JNI) wrapper. What this does is declare your C++ functions and classes so the Java virtual machine is able to import them. More info here. You can take a look at some more examples here if you want more information.

Installing Rosjava and Android core

Before we proceed you need to install rosjava and the ros android core in your system. The following tutorials will help you on that:

  1. Installing Rosjava

  2. Installing Android core

Our native code will be wrapped extending the NativeNodeMain class, which is also available on rosjava_mvn_repo since version 0.3.1 of rosjava_core.

Note: We will use some tools included in rosjava_build_tools, so be sure to source your devel/setup.bash of your rosjava workspace if you installed it from source. If you installed rosjava from debian packages, sourcing the standard /opt/ros/<distro>/setup.bash is enough.

Creating an Android project

1. Create a new workspace folder (we will use your home directory for simplicity along this tutorial):

$ mkdir -p ~/android1/src

$ cd ~/android1/src

2. Create a new Android package including the needed dependencies:

$ catkin_create_android_pkg androidpkg1 android_core, rosjava_core, std_msgs

3. You can comment-out the following lines from the file "CMakeLists.txt". If you don’t don’t wan’t to create a maven repo:

# Deploy android libraries (.aar's) and applications (.apk's)
install(DIRECTORY {CATKIN_DEVEL_PREFIX}/${CATKIN_GLOBAL_MAVEN_DESTINATION}/com/github/rosjava/${PROJECT_NAME}/ DESTINATION {CATKIN_GLOBAL_MAVEN_DESTINATION}/com/github/rosjava/${PROJECT_NAME}/)

4. Create an new Android project for this package:

$ cd androidpkg1
$ catkin_create_android_project androidp1

5. This just created an empty java template for you to work on:

~/androidp1/src/main/java/com/github/rosjava/android/androidp1/Androidp1.java

6. Edit "CMakeLists.txt". Replace the "assembleRelease" target for the "assembleDebug" target (this is better for testing purposes).

$ cd ~/android1/src/androidpkg1
$ vim CMakeLists.txt

catkin_android_setup(assembleDebug uploadArchives)

7. From the top level directory run:

$ catkin_make

8. After the build you should have the compiled code located in:

~/android1/src/androidpkg1/androidp1/build/outputs/apk

To build with rosjava android_core add the following dependencies to your "androidp1/build.gradle" (NOTE: these usually are commented-out!!):

dependencies {
  compile('org.ros.android_core:android_10:[0.3, 0.4)') {
    exclude group: 'junit'
    exclude group: 'xml-apis'
  }
}

Creating the JNI wrappers for your library

For this tutorial, we are going to wrap a simple publisher node just like the one in this very basic example

1. After the previous steps you should have a project structure as follows:

~/android1/src/androidpkg1/androidp1/src/main/java

Go to that directory and create the standard Java folders for this code:

cd ~/android1/src/androidpkg1/androidp1/src/main/java
mkdir -p org/ros/rosjava_tutorial_native_node

2. Write code for a Rosjava node extending the "NativeNodeMain.java" in your directory created in the step above. For example:

   1 package org.ros.rosjava_tutorial_native_node;
   2 
   3 import org.ros.node.NativeNodeMain;
   4 import org.ros.namespace.GraphName;
   5 
   6 /**
   7  * Class to implement a chatter native node.
   8  **/
   9 public class ChatterNativeNode extends NativeNodeMain {
  10   private static final String libName = "chatter_jni";
  11   public static final String nodeName = "chatter";
  12 
  13   public ChatterNativeNode() {
  14     super(libName);
  15   }
  16 
  17   public ChatterNativeNode(String[] remappingArguments) {
  18     super(libName, remappingArguments);
  19   }
  20   
  21   @Override
  22   public GraphName getDefaultNodeName() {
  23     return GraphName.of(nodeName);
  24   }
  25 
  26   @Override
  27   protected native int execute(String rosMasterUri, String rosHostname, String rosNodeName, String[] remappingArguments);
  28 
  29   @Override
  30   protected native int shutdown();
  31 }

Our node class extends the "NativeNodeMain" class that provides functionality to load and execute the native code. In the constructor we pass on to the superclass the name of the native library to load:

   9  **/
  10 public class ChatterNativeNode extends NativeNodeMain {
  11   private static final String libName = "chatter_jni";
  12   public static final String nodeName = "chatter";
  13 
  14   public ChatterNativeNode() {
  15     super(libName);

Then, we have the declaration of the actual native methods. These match the ones that will be declared in the C++ code:

  25   }
  26 
  27   @Override
  28   protected native int execute(String rosMasterUri, String rosHostname, String rosNodeName, String[] remappingArguments);
  29 
  30   @Override

3. Now we need to create a folder for our native code:

$ cd ~/android1/src/androidpkg1/androidp1/src/main
$ mkdir -p jni/src

4. Standing in this new folder we will proceed to generate a header file for our native code based on the declaration in the java source. It's important to compile the java class before this step (run catkin_make from your top level workspace directory).

$ javah -o chatter_jni.h -classpath /mydir/src/androidpkg1/androidp1/build/intermediates/classes/debug org.ros.rosjava_tutorial_native_node.ChatterNativeNode

The classpath must have the full path (no relative paths are allowed). Note: if you installed rosjava from source, you might need to specify the path to your compiled NativeNodeMain class too. Something like this:

$ javah -o chatter_jni.h -classpath ~/android1/src/androidpkg1/androidp1/build/intermediates/classes/debug:~/rosjava/src/rosjava_core/rosjava/build/classes/main/ org.ros.rosjava_tutorial_native_node.ChatterNativeNode org.ros.node.NativeNodeMain

(i.e. use the directory where you installed rosjava, appending it to the first classpath using ":").

We should now have the file "chatter_jni.h" with the declaration of the exported functions and their parameters according to the Java signature. The file should look something like this:

   1 /* DO NOT EDIT THIS FILE - it is machine generated */
   2 #include <jni.h>
   3 /* Header for class org_ros_rosjava_tutorial_native_node_ChatterNativeNode */
   4 
   5 #ifndef _Included_org_ros_rosjava_tutorial_native_node_ChatterNativeNode
   6 #define _Included_org_ros_rosjava_tutorial_native_node_ChatterNativeNode
   7 #ifdef __cplusplus
   8 extern "C" {
   9 #endif
  10 /*
  11  * Class:     org_ros_rosjava_tutorial_native_node_ChatterNativeNode
  12  * Method:    execute
  13  * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)V
  14  */
  15 JNIEXPORT jint JNICALL Java_org_ros_rosjava_1tutorial_1native_1node_ChatterNativeNode_execute
  16   (JNIEnv *, jobject, jstring, jstring, jstring, jobjectArray);
  17 
  18 /*
  19  * Class:     org_ros_rosjava_tutorial_native_node_ChatterNativeNode
  20  * Method:    shutdown
  21  * Signature: ()V
  22  */
  23 JNIEXPORT jint JNICALL Java_org_ros_rosjava_1tutorial_1native_1node_ChatterNativeNode_shutdown
  24   (JNIEnv *, jobject);
  25 
  26 #ifdef __cplusplus
  27 }
  28 #endif
  29 #endif
  30 

5. Code the functions declared in header file. These functions should encapsulate the calls to the actual library. For example the following (chatter_jni.cpp):

   1 #include <android/log.h>
   2 #include <ros/ros.h>
   3 
   4 #include "chatter_jni.h"
   5 
   6 #include "std_msgs/String.h"
   7 #include <sstream>
   8 
   9 using namespace std;
  10 
  11 void log(const char *msg, ...) {
  12     va_list args;
  13     va_start(args, msg);
  14     __android_log_vprint(ANDROID_LOG_INFO, "Native_Chatter", msg, args);
  15     va_end(args);
  16 }
  17 
  18 inline string stdStringFromjString(JNIEnv *env, jstring java_string) {
  19     const char *tmp = env->GetStringUTFChars(java_string, NULL);
  20     string out(tmp);
  21     env->ReleaseStringUTFChars(java_string, tmp);
  22     return out;
  23 }
  24 
  25 bool running;
  26 
  27 JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
  28     log("Library has been loaded");
  29     // Return the JNI version
  30     return JNI_VERSION_1_6;
  31 }
  32 
  33 JNIEXPORT jint JNICALL Java_org_ros_rosjava_1tutorial_1native_1node_ChatterNativeNode_execute(
  34         JNIEnv *env, jobject obj, jstring rosMasterUri, jstring rosHostname, jstring rosNodeName,
  35         jobjectArray remappingArguments) {
  36     log("Native chatter node started.");
  37     running = true;
  38 
  39     string master("__master:=" + stdStringFromjString(env, rosMasterUri));
  40     string hostname("__ip:=" + stdStringFromjString(env, rosHostname));
  41     string node_name(stdStringFromjString(env, rosNodeName));
  42 
  43     log(master.c_str());
  44     log(hostname.c_str());
  45 
  46     // Parse remapping arguments
  47     log("Before getting size");
  48     jsize len = env->GetArrayLength(remappingArguments);
  49     log("After reading size");
  50 
  51     std::string ni = "chatter_jni";
  52 
  53     int argc = 0;
  54     const int static_params = 4;
  55     char **argv = new char *[static_params + len];
  56     argv[argc++] = const_cast<char *>(ni.c_str());
  57     argv[argc++] = const_cast<char *>(master.c_str());
  58     argv[argc++] = const_cast<char *>(hostname.c_str());
  59 
  60     //Lookout: ros::init modifies argv, so the references to JVM allocated strings must be kept in some other place to avoid "signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr deadbaad"
  61     // when trying to free the wrong reference ( see https://github.com/ros/ros_comm/blob/indigo-devel/clients/roscpp/src/libros/init.cpp#L483 )
  62     char **refs = new char *[len];
  63     for (int i = 0; i < len; i++) {
  64         refs[i] = (char *) env->GetStringUTFChars(
  65                 (jstring) env->GetObjectArrayElement(remappingArguments, i), NULL);
  66         argv[argc] = refs[i];
  67         argc++;
  68     }
  69 
  70     log("Initiating ROS...");
  71     ros::init(argc, &argv[0], node_name.c_str());
  72     log("ROS intiated.");
  73 
  74     // Release JNI UTF characters
  75     for (int i = 0; i < len; i++) {
  76         env->ReleaseStringUTFChars((jstring) env->GetObjectArrayElement(remappingArguments, i),
  77                                    refs[i]);
  78     }
  79     delete refs;
  80     delete argv;
  81 
  82     ros::NodeHandle n;
  83     ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);
  84 
  85     ros::Rate loop_rate(1);
  86 
  87     int count = 0;
  88     while (ros::ok()) {
  89         /**
  90          * This is a message object. You stuff it with data, and then publish it.
  91          */
  92         std_msgs::String msg;
  93 
  94         std::stringstream ss;
  95         ss << "hello world " << count;
  96         msg.data = ss.str();
  97 
  98         ROS_INFO("%s", msg.data.c_str());
  99 
 100         /**
 101          * The publish() function is how you send messages. The parameter
 102          * is the message object. The type of this object must agree with the type
 103          * given as a template parameter to the advertise<>() call, as was done
 104          * in the constructor above.
 105          */
 106         chatter_pub.publish(msg);
 107 
 108         ros::spinOnce();
 109 
 110         loop_rate.sleep();
 111         ++count;
 112     }
 113 
 114     log("Exiting from JNI call.");
 115     return 0;
 116 }
 117 
 118 JNIEXPORT jint JNICALL Java_org_ros_rosjava_1tutorial_1native_1node_ChatterNativeNode_shutdown
 119         (JNIEnv *, jobject) {
 120     log("Shutting down native node.");
 121     ros::shutdown();
 122     running = false;
 123     return 0;
 124 }

6. We have to create the "Android.mk" and "Application.mk" configuration files:

$ cd ~/android1/src/androidpkg1/androidp1/src/main/jni
$ vim Android.mk
$ vim Application.mk

Android.mk example:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE    := chatter_jni
LOCAL_SRC_FILES := src/chatter_jni.cpp
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
LOCAL_LDLIBS := -landroid -llog
LOCAL_STATIC_LIBRARIES := roscpp_android_ndk
include $(BUILD_SHARED_LIBRARY)

$(call import-add-path, /home/user/ros-android-ndk/roscpp_android/output/)
$(call import-module, roscpp_android_ndk)

The "LOCAL_STATIC_LIBRARIES" variable defines where the cross-compiled libraries are located. You can recall the previous tutorials for this information.

Note: the line calling import-add-path should have the path to your output directory of your locally installed roscpp_android_ndk environment.

Application.mk example:

#NDK_TOOLCHAIN_VERSION=4.4.3
APP_STL := gnustl_static
APP_PLATFORM := android-15

Here we define the C++ runtime library we want to use and the Android SDK version we are targeting.

7. Going back up in the directory structure we must edit the "build.gradle" file to add the new dependencies:

$ cd mydir/src/androidpkg1/androidp1
$ vim build.gradle

Add the ndkBuild task to your file, as well as the sourceSets block to your android block.

dependencies {
  compile('org.ros.android_core:android_10:[0.3, 0.4)') {
    exclude group: 'junit'
    exclude group: 'xml-apis'
  }
}

tasks.withType(JavaCompile) {
    compileTask -> compileTask.dependsOn ndkBuild
}

task ndkBuild(type: Exec) {
    Properties properties = new Properties()
    properties.load(project.rootProject.file('local.properties').newDataInputStream())
    def ndkbuild = properties.getProperty('ndk.dir', null) + "/ndk-build"
    commandLine ndkbuild, '-C', file('src/main/jni').absolutePath
}

android {
...
    sourceSets.main {
        jniLibs.srcDir 'src/main/libs'
        jni.srcDirs = [];
    }
...
}

You also need to add a file named "local.properties" in the androidpkg1 directory. It should specify the path to your SDK and NDK install directories; for example:

ndk.dir=/home/user/Android/Sdk/ndk-bundle
sdk.dir=/home/user/Android/Sdk

If you used Android Studio to install SDK & NDK, the default installation path is ~/Android/Sdk; use the absolute path in local.properties.

What's happening here? This task added to your Gradle script will call the Android NDK buildsystem with your recently created Android.mk file as an argument. Then, it will build your code following the rules specified in Android.mk to create a Shared Library (.so file), and place it inside the resulting apk file. In other words, your Java code and your native code will be built and packed together with a single command!

Writing a test app

1. Lets write something interesting to test our native node:

   1 package com.github.rosjava.android.androidp1;
   2 
   3 import org.ros.android.RosActivity;
   4 import org.ros.node.NodeConfiguration;
   5 import org.ros.node.NodeMainExecutor;
   6 import org.ros.RosCore;
   7 
   8 import org.apache.commons.logging.Log;
   9 import org.apache.commons.logging.LogFactory;
  10 
  11 import org.ros.rosjava_tutorial_native_node.ChatterNativeNode;
  12 import java.net.URI;
  13 
  14 public class Androidp1 extends RosActivity
  15 {
  16     private RosCore myRoscore;
  17     private Log log = LogFactory.getLog(Androidp1.class);
  18     private NodeMainExecutor nodeMainExecutor = null;
  19     private URI masterUri;
  20     private String hostName;
  21     private ChatterNativeNode chatterNativeNode;
  22     final static String appName = "native_wrap_test";
  23     
  24     public Androidp1()
  25     {
  26         super(appName, appName);        
  27     }
  28     
  29     @Override
  30     protected void init(NodeMainExecutor nodeMainExecutor)
  31     {
  32         log.info("Androidp1 init");
  33         
  34         // Store a reference to the NodeMainExecutor and unblock any processes that were waiting
  35         // for this to start ROS Nodes
  36         this.nodeMainExecutor = nodeMainExecutor;
  37         masterUri = getMasterUri();
  38         hostName = getRosHostname();
  39 
  40         log.info(masterUri);
  41 
  42         startChatter();        
  43     }
  44     
  45     // Create a native chatter node
  46     private void startChatter()
  47     {
  48         log.info("Starting native node wrapper...");
  49         
  50         NodeConfiguration nodeConfiguration = NodeConfiguration.newPublic(hostName);
  51         
  52         nodeConfiguration.setMasterUri(masterUri);
  53         nodeConfiguration.setNodeName(ChatterNativeNode.nodeName);
  54         
  55         chatterNativeNode = new ChatterNativeNode();
  56         
  57         nodeMainExecutor.execute(chatterNativeNode, nodeConfiguration);
  58     }    
  59 }

2. Before compiling the code, we should update the AndroidManifest located in ~/android1/src/androidpkg1/src/main. Copy the following into your manifest file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.github.rosjava.android.androidp1"
      android:versionCode="1"
      android:versionName="1.0">

    <uses-permission android:name="android.permission.INTERNET" />

    <application android:label="@string/app_name">
        <activity android:name="Androidp1"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name="org.ros.android.MasterChooser" />
        <service android:name="org.ros.android.NodeMainExecutorService" >
            <intent-filter>
                <action android:name="org.ros.android.NodeMainExecutorService" />
            </intent-filter>
        </service>

    </application>
</manifest>

This allows the application to use the MasterChooser defined in the RosActivity, and connect to a ROS master using its IP address.

3. We can now go to the top level folder of our project and run catkin_make; if everything works well we should get our Android package file ready to install and run.

$ cd mydir
$ catkin_make

Find the resulting package, and install it to your device:

cd ~/android1/src/androidpkg1/androidp1/build/outputs/apk/
adb install androidp1-debug.apk

You should be able to find the a copy of the built shared library here inside the apk file (look for libchatter_jni.so inside the lib directory after opening the apk).

4. Run the app! After connecting to a ROS master, you should be able to listen to the "chatter" topic, which will print a message per second.

Extras

Upstreaming error codes to your application

As you may have noticed, the native methods defined above return an integer. This return value is intended to be an error code that you can handle from the Java side of your application.

If your native execute or shutdown methods return a value different than 0, your node's onError method will be called. You can override this method in your native node and access the protected variables executeReturnCode and shutdownReturnCode (inherited from NativeNodeMain) to handle these cases. For example:

   1 // In your ChatterNativeNode class
   2     @Override
   3     public void onError(Node node, Throwable throwable) {
   4         if (super.executeReturnCode != 0) {
   5             // Handle execution error
   6         } else if (super.shutdownReturnCode != 0 {
   7             // Handle shutdown error
   8         }
   9     }

Under the hood

Your Java code (ChatterNativeNode in this example) is a pure rosjava node that ends up calling your defined execute method in its onStart method. Note that the native code also creates a new node which connects to the master. If both nodes have the same name like in this case, the ROS master will disconnect the first one connected -the pure Java node-. You can try creating the node with a different name changing the configuration in startChatter, and you should see two new nodes after running the app.

This doesn't affect functionality at all, but it's something to keep in mind when handling the error codes as described above. In case of an error, the onError method will be called after the node is disconnected. If you need to handle the error being connected to the ROS master, don't use the same name for both nodes (pure Java and native).

Wiki: android_ndk/Tutorials/WrappingNativeRosjavaNode (last edited 2017-02-24 20:20:05 by jubeira)