Translate

Saturday, May 19, 2012

Development on Android NDK Tutorial - Gluing C++ Native Code to Java

Too busy to draw this time.. for now here's a picture of an Android Toy



Ok, in this post I am going to talk about how to join C++ native code to a thin Android Java code layer.  I am not going to be doing all native code, as I think the easiest route is to use the Android higher level convenience SDK for loading resources/file I/O, setting up OpenGL context and other state management.

I won't be using any IDEs or anything else to complicate things.  This is as easy as it can get, you get the source and type "make" (I assume you are on a system with GNU make, and your environment is setup correctly.  Linux is easiest, but it should run fine on ANY system that can run Make, even Windows!).

So the code will be structured like this:

-----------------------------------
 Android SDK OS Layer code
-----------------------------------
  Java App Thin Layer to Android OS
-----------------------------------
  Native C++ code called by Java
  for each frame                          
-----------------------------------


The Native C++ code is where we are going to be doing the meat of our programming.  This is where we manage any OpenGL calls (graphics stuff, any local matrix manipulations etc), collision detection (borrowing Box2D for this example) and game state management.

I have posted a repository for this posting here, so you can check out the code and play.  I tried to keep it as simple as possible but feel free to ask questions.

You will notice the typical OpenGL context skeleton code in this example (under Physics2d.java), it is something like this:

  ...
 31 public class Physics2d extends Activity
 32 {
 33
 34   private GLSurfaceView glSurface;
 35   private MyRenderer mRenderer;
 36
 37
 38   @Override 
 39   public void onCreate(Bundle savedInstanceState)
 40   {
 41     super.onCreate(savedInstanceState);
 42
 43     glSurface = new GLSurfaceView(this);
 44     mRenderer = new MyRenderer(getApplication());
 45     glSurface.setRenderer(mRenderer);
 46
 47     // Turn off app title
 48     requestWindowFeature(Window.FEATURE_NO_TITLE);
 49
 50     getWindow().setFlags(
 51         WindowManager.LayoutParams.FLAG_FULLSCREEN,
 52         WindowManager.LayoutParams.FLAG_FULLSCREEN);
 53
 54     setContentView(glSurface);
 55
 56     Display display = getWindowManager().getDefaultDisplay();
 57
 58     Log.e("Screen Size", "Screen dimensions: {" +
 59         display.getWidth() + ", " +
 60         display.getHeight() +
 61         "}");
 62   }
 63
 64   @Override
 65   public void onResume()
 66   {
 67     super.onResume();
 68     glSurface.onResume();
 69   }
 70
 71   @Override 
 72   public void onPause()
 73   {
 74     super.onPause();
 75     glSurface.onPause();
 76   }
 77
 78   static
 79   {
 80     System.loadLibrary("main");
 81   }
 82 }
 83
 84 class MyRenderer implements Renderer
 85 {
 86
 87   Application application = null;
 88   private String apk_file = null;
 89
 90   public MyRenderer(Application a)
 91   {
 92
 93     application = a;
 94   }
 95
 96   @Override
 97   public void onSurfaceCreated(GL10 gl, EGLConfig config)
 98   {
 99
100     Texture.setGlContext(gl);
101     ResourceLoadingTools.setApplication(application);
102
103     initializeGL();
104
105   }
...
109   @Override
110   public void onSurfaceChanged(GL10 gl, int width, int height)
111   {
112
113     Texture.setGlContext(gl);
114     ResourceLoadingTools.setApplication(application);
115
116     //Texture.glLoadAlphaStatic("box.png");
117
118     gl.glViewport(0, 0, width, height);
119
120     gl.glMatrixMode(GL10.GL_PROJECTION);
121     gl.glLoadIdentity();
122     float aspect = (float)width / (float)height;
123
124     gl.glMatrixMode(GL10.GL_MODELVIEW);
125     gl.glLoadIdentity();
126     //gl.glTranslatef(0.0f, 0.0f, -20.0f);
127     resizeGL(width, height);
128   }
129
130   @Override
131   public void onDrawFrame(GL10 gl)
132   {
133
134     Texture.setGlContext(gl);
135     ResourceLoadingTools.setApplication(application);
136
137     paintGL();
138     //eglSwapBuffers();
139   }
140
141   private native void initializeGL();
142   private native void resizeGL(int w, int h);
143   private native void paintGL();
144
145 }

You see the usual Android skeleton functions onSurfaceChanged(), *Created(), onPause etc.  However what are these declarations with the keyword native all about?  This is indicator that the implementation for these methods will be defined in Native code via JNI.

Aha, these will be our window to the wonderful world of (potentially) faster native code!

How do these translate to C++ function names?

For example, in Physics2d.java:
141   private native void initializeGL();

Is translated to the C++ equivalent function name of:

JNIEXPORT void JNICALL Java_com_example_Monkey_Poop_MyRenderer_initializeGL(
      JNIEnv* env, jclass cls)


What happened here, how did we get such a convoluted name?


Well, It appears Java is always at the front of a JNI function name.

The middle part, _com_example_Monkey_Poop_  is a reference to the package name (which I picked a ridiculous one, yeah, I'm 5 years old and poop games are funny, Monkeys + Poop_chucking = epic).

The next part MyRenderer is a reference to the Java class the JNI declaration was in, and lastly the initializeGL is the Java function name.

Cool, so how is it all resolved at run time?  Well, I am no Java expert but it appears the line below loads the libmain library (where I have JNI functions):

 78   static
 79   {
 80     System.loadLibrary("main");
 81   }

Thus, it resolves the function at run time and maps our Java call to the C++ native call with the funky name.

What about passing in parameters to C++ via Java?


Oh, I knew you'd ask that.  Ok, lets look at example:

142   private native void resizeGL(int w, int h);

Here we are passing in two integers, but when we see the translated C++ function signature, it looks like this:

JNIEXPORT void JNICALL Java_com_example_Monkey_Poop_MyRenderer_resizeGL(
      JNIEnv* env, jobject obj, jint w, jint h)

Look right after the jobject parameter, we have two more parameters than the last JNI function we looked at, w and h.  Ah, and we can see the primitives translate from Java int to a C++ type of jint, which is actually just a typedef (in most cases) to a standard int.  So we can safely cast to a C++ primitive and use them as needed.

Eg.:
float aspect = (float)w / (float)h; 

If you want to know more about JNI, passing around more crazy types, I suggest checking out some docs on JNI from Sun/Oracle. 

What about passing in parameters to Java Android OS layer code via C++?


Oh, this is more interesting here.  For this example, I wanted to load a texture from the Android package, using normal SDK Java calls, but I want to be able to load these resources from the Game logic in C++ land.

We do loading of APK sources using SDK calls since there really isn't a good Native interface on older Android NDK for this.  Only hacks of using libzip and iterating on your own package name (but I am not sure how portable this is, package location, naming etc).  It seems much more portable to use the native SDK for OS types of things like file I/O.

So first let's code and test a Java OpenGL texture loader.  (I just make life easier and right after loading create the texture in GPU memory and delete image resources).


Texture.java
 34
 35 /**
 36  *   Just a wrapper for creating textures and sending to native
 37  */
 38
 39 public class Texture
 40 {
 41
 42   static GL10 gl_context = null;
 43
 44   private Bitmap bitmap = null;
 45
 46   //**************************************************************************
 47   /**
 48    *  Keep OpenGL context for application (will change)
 49    */
 50
 51   static public void setGlContext(GL10 gl)
 52   {
 53
 54     gl_context = gl;
 55   }
 56
 57   //**************************************************************************
 58   /** Loads the resource to memory
 59    *
 60    */
 61
 62   static public int glLoadAlphaStatic(String file) throws java.io.IOException
 63   {
 64
 65     if(gl_context == null)
 66     {
 67
 68       Log.e("glLoadAlphaStatic(" + file + ")", " gl_context was null!\n");
 69       return -1;
 70
 71     }
 72
 73     GL10 gl = gl_context;
 74
 75     Bitmap bitmap = null;
 76     try
 77     {
 78
 79       bitmap = ResourceLoadingTools.loadImage(file);
 80     }
 81     catch(java.io.FileNotFoundException e)
 82     {
 83       Log.e("ERROR",
 84           "---------------------------------------------------------------\n");
 85       Log.e("ERROR", "Could not FIND image: " + file +
 86           " from apk file!\n");
 87       Log.e("ERROR",
 88           "---------------------------------------------------------------\n");
 89
 90       // Rethrow so application fails with better stack trace
 91       e.printStackTrace();
 92       throw e;
 93       //return -1;
 94     }
 95     catch(java.io.IOException e)
 96     {
 97       Log.e("ERROR",
 98           "---------------------------------------------------------------\n");
 99       Log.e("ERROR", "Could not load image: " + file +
100           " from apk file!\n");
101       Log.e("ERROR",
102           "---------------------------------------------------------------\n");
103       // Rethrow so application fails with better stack trace
104       e.printStackTrace();
105       throw e;
106       //return -1;
107     }
108
109     Log.e("SUCCESS",
110         "---------------------------------------------------------------\n");
111     Log.v("SUCCESS", "Loaded file: " + file + "!\n");
112     Log.e("SUCCESS",
113         "---------------------------------------------------------------\n");
114
115
116     int textureName = -1;
117     int[] texture = new int[1];
118     gl.glGenTextures(1, texture, 0);
119     textureName = texture[0];
120
121     //Log.d(TAG, "Generated texture: " + textureName);
122     gl.glBindTexture(GL10.GL_TEXTURE_2D, textureName);
123     gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
124     gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
125     gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
126         GL10.GL_CLAMP_TO_EDGE);
127     gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
128         GL10.GL_CLAMP_TO_EDGE);
129     //gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_REPLACE);
130     gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_MODULATE);
131
132     ByteBuffer image_buff = ByteBuffer.allocateDirect(
133         bitmap.getHeight() * bitmap.getWidth() * 4);
134     image_buff.order(ByteOrder.nativeOrder());
135     byte buffer[] = new byte[4];
136
137     for(int y = 0; y < bitmap.getHeight(); y++)
138     {
139
140       for(int x = 0; x < bitmap.getWidth(); x++)
141       {
142
143         int color = bitmap.getPixel(x, y);
144         buffer[0] = (byte)Color.red(color);
145         buffer[1] = (byte)Color.green(color);
146         buffer[2] = (byte)Color.blue(color);
147         buffer[3] = (byte)Color.alpha(color);
148         image_buff.put(buffer);
149
150       }
151
152     }
153
154     image_buff.position(0);
155     gl.glTexImage2D(GL10.GL_TEXTURE_2D, 0, GL10.GL_RGBA,
156         bitmap.getWidth(), bitmap.getHeight(), 0,
157         GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, image_buff);
158
159     //GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, bitmap, 0);
160
161     //    mCropWorkspace[0] = 0;
162     //    mCropWorkspace[1] = bitmap.getHeight();
163     //    mCropWorkspace[2] = bitmap.getWidth();
164     //    mCropWorkspace[3] = -bitmap.getHeight();
165     //
166     //    ((GL11) gl).glTexParameteriv(GL10.GL_TEXTURE_2D,
167     //      GL11Ext.GL_TEXTURE_CROP_RECT_OES, mCropWorkspace, 0);
168
169     int error = gl.glGetError();
170     if (error != GL10.GL_NO_ERROR)
171     {
172
173       Log.e(Logging.TAG, "GL Texture Load Error: " + error);
174       return textureName;
175     }
176
177     int width = bitmap.getWidth();
178     int height = bitmap.getHeight();
179
180     // Reclaim bitmap memory
181     bitmap.recycle();
182     bitmap = null;
183
184     Log.e("SUCCESS",
185         "---------------------------------------------------------------\n");
186     Log.v("SUCCESS", "Loaded file as texture: " + textureName + "!\n");
187     Log.e("SUCCESS",
188         "---------------------------------------------------------------\n");
189
190
191     //Log.d(TAG, "Loaded texture: " + textureName);
192     return textureName;
193   }
194
...
430
431 }

Ok, that wasn't too bad, we loaded an image into OpenGL using easy to use SDK functions and the texture handle is created. Now, how will C++ know about the new texture?

Well, we should call this Java function from C++ and get the return value (texture handle).


Texture.cpp
  //*****************************************************************************
 76 /**
 77  *  Grab a image resource from apk file and load in OpenGL context
 78  *
 79  *  @return texture ID from opengl context
 80  */
 81
 82 int Texture::loadTexture(const char* filename)
 83 {
 84
 85   if(filename == NULL)
 86   {
 87
 88     LOGE("filename passed was NULL!\n");
 89     return -1;
 90   }
 91   LOGI("Texture::loadTexture(%s) called\n",
 92       filename);
 93
 94   const char CLASS_NAME[]  = "Texture";
 95   const char METHOD_NAME[]  = "glLoadAlphaStatic";
 96   int texture_num = -1;
 97   AndroidJNIHelper helper = AndroidJNIHelper::getJNIHelper();
 98   JNIEnv* env = helper.getJNIEnv();
 99   if(env == NULL)
100   {
101
102     LOGE("JNIEnv was NULL!\n");
103     return -1;
104   }
105
106   string class_path = helper.getJavaPackagePath() + CLASS_NAME;
107   jclass cls = env->FindClass(class_path.c_str());
108   LOGD("class_path: %s\n", class_path.c_str());
109
110   jmethodID mid = env->GetStaticMethodID(cls,
111       METHOD_NAME,
112       "(Ljava/lang/String;)I");
113
114   if(mid == 0)
115   {
116
117     LOGE("Unable to load method: %s:%s\n",
118         CLASS_NAME,
119         METHOD_NAME);
120     return -1;
121   }
122
123   jint ret_val = -1;
124   jstring mystr = env->NewStringUTF(filename);
125   ret_val = env->CallStaticIntMethod(cls, mid, mystr);
126   return ret_val;
127
128 }


jni/AndroidJNIHelper.h
  1
  2 #ifndef ANDROIDJNIHELPER_H
  3 #define ANDROIDJNIHELPER_H
  4
  5 #include <string>
  6 #include <jni.h>
  7 #include <GLES/gl.h>
  8 #include <math.h>
  9 #include <new>
 10 #include <zip.h>
 11 #include "def.h"
 12 #include "utils.h"
 13 #include "GLHelpers.h"
 14
 15
 16 /**
 17  *   @file AndroidJNIHelper.cpp
 18  *    
 19  *
 20  *    Create an easy to use way to grab jni environment
 21  *    
 22  *    JNI method and constructor signature cheat sheet
 23  *
 24  *  B=byte
 25  *  C=char
 26  *  D=double
 27  *  F=float
 28  *  I=int
 29  *  J=long
 30  *  S=short
 31  *  V=void
 32  *  Z=boolean
 33  *  Lfully-qualified-class=fully qualified class
 34  *  [type=array of type>
 35  *  (argument types)return type=method type. 
 36  *     If no arguments, use empty argument types: ().
 37  *     If return type is void (or constructor) use (argument types)V.*    
 38  *
 39  *     Example
 40  *     @code
 41  *     constructor:
 42  *     (String s)
 43  *
 44  *     translates to:
 45  *     (Ljava/lang/String;)V
 46  *
 47  *     method:
 48  *     String toString()
 49  *
 50  *     translates to:
 51  *     ()Ljava/lang/String;
 52  *
 53  *     method:
 54  *     long myMethod(int n, String s, int[] arr)
 55  *
 56  *     translates to:
 57  *     (ILjava/lang/String;[I)J
 58  *     @endcode
 59  *
 60  */
 61
 62 //*****************************************************************************
 63 /**
 64  *   Singleton class for helping interface with Java Native Interface
 65  */
 66
 67 class AndroidJNIHelper
 68 {
 69
 70   private:
 71
 72     /// The current JNI context
 73     JNIEnv* jni_env;
 74
 75     const char* java_package_path;
 76
 77     AndroidJNIHelper():
 78       jni_env(NULL)
 79   {
 80
 81   }
 82
 83   public:
 84     static AndroidJNIHelper& getJNIHelper()
 85     {
 86
 87       static const char JAVA_PACKAGE_PATH[] = "com/example/Monkey/Poop/";
 88       static AndroidJNIHelper a;
 89       a.java_package_path = JAVA_PACKAGE_PATH;
 90       return a;
 91
 92     }
 93
 94     std::string getJavaPackagePath()
 95     {
 96
 97       return (std::string)java_package_path;
 98     }
 99
100     void setJNIEnv(JNIEnv* env)
101     {
102
103       jni_env = env;
104     }
105
106     JNIEnv* getJNIEnv()
107     {
108
109       return jni_env;
110     }
111
112     int jniLoadTexture(const char* s);
113
114
115 };
116
117
118 #endif /* ANDROIDJNIHELPER_H */

Hmm, so AndroidJNIHelper singleton class stores the JNIEnv* context, which is updated everytime a JNI function gets called (which in our case is every frame).  This is just in case our entire context gets pulled out from underneath us (which happens all the time for OpenGL context, not sure about JNI so store it anyhow, it costs next to nothing to do so!). Also, I hardcoded the package path there, so you may want to make it more flexible and extend it! :)

We use the helper class to get access to the latest valid JNIEnv context and this context is used to lookup the Java class/method we would like to call from C++:

107   jclass cls = env->FindClass(class_path.c_str());

This line indicates which method you are looking for:

110   jmethodID mid = env->GetStaticMethodID(cls,
111       METHOD_NAME,
112       "(Ljava/lang/String;)I");

This code actually calls the method we found in java code and saves the return value in ret_value:
125   ret_val = env->CallStaticIntMethod(cls, mid, mystr);

ret_val is our OpenGL texture handle that we created in Java/SDK land!  You can use this as you normally would to texture polygons.  I show texturing during my Renderer::drawFrame() C++ function, but note that I hard coded it as "1" for simplicity, and since we are only dealing with 1 texture handle, it will always be "1". (OpenGL is very predictable in how it issues out texture handles/ids).  I will extend it to use the Texture::texture_id for a later example.

The indicator for a Java String parameter ("(Ljava/lang/String;)I")  is odd, I know, but this is what Sun/Oracle decided on how to resolve types.

The (Ljava/lang/String;) part indicates the function we are looking for takes a java.lang.String as a a parameter (the parenthesis indicate this) and the I in the signature,  "(Ljava/lang/String;)I") indicates it returns an integer.

 Here is more info: http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/types.html  I also left some info in my comments on the AndroidJNIHelper.h file, which was borrowed from this cool fellow: http://dev.kanngard.net/Permalinks/ID_20050509144235.html

Ok, so when all is said and done, you should now have the tools to get started with an app that loads a texture via the normal Android Java SDK and can use it within Android Native C++ !!


Grab my code and play around and see how it all works:

svn checkout http://razzlegames-android-ndk-tutorial.googlecode.com/svn/trunk/ razzlegames-android-ndk-tutorial-read-only

Or you can browse here


Next time.... I'll get to GDB debugging!