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
- Knowledge of Android development
- Have the following software installed in your system:
- ROS environment
- Android SDK
- Android NDK
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:
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:
Then, we have the declaration of the actual native methods. These match the ones that will be declared in the C++ code:
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:
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).