Building a JNI library requires a basic understanding of JNI, a C source file, and the appropriate entries in the Android.mk file. The next few sections break down these requirements, step by step. We begin with a brief discussion of how code is mapped between the Java and C language environments.
19.3.1 Understanding JNI
As mentioned earlier, a JNI library exposes one or more functions to a Java-based application through a specific naming convention. A C language function is named according to the following guideline:
Java_fully_qualified_class_name_method_name
For example, a method named SomeMethod() in the class named DoSomething, which takes an integer and a string argument, would be defined as shown in the following listing.
JNIEXPORT jint JNICALL Java_com_somecompany_DoSomething_SomeMethod(
JNIEnv * env, jobject obj, jint i, jstring s);
The function is named according to the JNI standard, including the prefix of Java, fol- lowed by the fully qualified class name and the method name. Every exported JNI function has at minimum the same first two arguments. The first argument is a pointer to the Java Environment. See the jni.h header file shipped with the NDK for available functionality in the JNIEnv object. The next argument is a pointer to the this object. Any additional arguments follow these two standard arguments. In this case, there’s an integer argument, which is a data type of jint, and a String argu- ment of type jstring. Again, see the jni.h header file for a more complete view of the available data types.
Calling a JNI function from Java first requires that the library be loaded. This is accomplished with a call to System.loadLibrary, passing in the module name. The module name is the name of the shared library minus the lib prefix and the .so exten- sion. The next listing shows how to load this module and declare the sample method.
Listing 19.1 Sample JNI function signature
package com.somecompany;
public class SomeObject { public SomeObject {
native int SomeMethod(jint i,jstring s);
static {
System.loadLibrary("SomeModule");
} } }
In this simple example, note the package name and class name. These names com- bine to form a portion of the JNI function name. The JNI function named SomeMethod() is defined with a native qualifier. To gain access to this function, it must be loaded via a call to loadLibrary, passing in the module name of the library.
Much more is involved in the JNI specification, but you have enough here to get started on the sample application code.
19.3.2 Implementing the library
Finally, you get to look at the C code that implements the image-processing functions!
The code listings are broken into three logical sections: the header of the C file with macros and data type definitions, and then each of the two image-processing func- tions. First you see the header of the file ua2efindedges.c.
#include <jni.h>
#include <android/log.h>
#include <android/bitmap.h>
#define LOG_TAG "libua2efindedges"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG,__VA_ARGS__)
typedef struct {
uint8_t alpha;
uint8_t red;
uint8_t green;
uint8_t blue;
} argb;
The first header file included in the C source file is jni.h B. This file contains the required data types and macros for the JNI. Without this header file, data types would be unrecognized and the code would never compile. The Android NDK provides sup- port for a handful of Android subsystems, including the logging and bitmap handling, among others. Those headers are included as well C because they’re required for this
Listing 19.2 Calling a JNI function
Listing 19.3 ua2efindedges.c
jni header file
B
Log, bitmap headers
C
Logging macros
D
Structure for image handling
E
application. A few macros D aid in accessing the LogCat functionality. Because this application is dealing with image data where each pixel is defined as a 32-bit structure representing a Color object, a structure is defined to easily manage the pixel data E.
NOTE The bitmap functionality shown in this example requires Android 2.2 or later.
Let’s now discuss the image-processing routines. Don’t concern yourself with the details of these functions unless they’re of interest to you. The basic approach of this application is to pass Bitmap objects from the Java code to the JNI code. The pixel buf- fers are locked such that the memory is accessible to the C code for raw manipulation.
When the image processing is complete, the pixels are unlocked.
The first function is named converttogray() and takes two arguments. The first argument is an Android Bitmap of the style RGBA_8888, which means each pixel con- tains a byte representing the Alpha channel and then one byte each for Red, Green, and Blue values. Each value is represented by an integer ranging in value from 0 to 255. The second Bitmap is created as a grayscale, 8-bits-per-pixel image. The first parameter is the input image, and the second is the output image. The following list- ing contains the converttogray() method. Note the long function name!
JNIEXPORT void JNICALL
Java_com_msi_manning_ua2efindedges_UA2EFindEdges_
➥converttogray(
JNIEnv * env, jobject obj,
jobject bitmapcolor, jobject bitmapgray) {
AndroidBitmapInfo infocolor;
void* pixelscolor;
AndroidBitmapInfo infogray;
void* pixelsgray;
int ret;
int y;
int x;
if ((ret = AndroidBitmap_getInfo(env, bitmapcolor, &infocolor)) < 0) {
LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
return;
}
if ((ret = AndroidBitmap_getInfo(env, bitmapgray, &infogray)) < 0) {
LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
return;
}
LOGI("color image :: width is %d; height is %d; stride is %d; format is
%d;flags is %d",infocolor.width,infocolor.height,infocolor.stride, Listing 19.4 converttogray function implementation
Specify function name
B
Define
AndroidBitmapInfo structure
Contain pointer C
to pixels
D
Get Bitmap info
E
Get Bitmap info
E
infocolor.format,infocolor.flags);
if (infocolor.format != ANDROID_BITMAP_FORMAT_RGBA_8888) { LOGE("Bitmap format is not RGBA_8888 !");
return;
}
LOGI("gray image :: width is %d; height is %d; stride is %d; format is
%d;flags is %d",infogray.width,infogray.height,infogray.stride, infogray.format,infogray.flags);
if (infogray.format != ANDROID_BITMAP_FORMAT_A_8) { LOGE("Bitmap format is not A_8 !");
return;
}
if ((ret = AndroidBitmap_lockPixels(env, bitmapcolor, &pixelscolor)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
}
if ((ret = AndroidBitmap_lockPixels(env, bitmapgray, &pixelsgray)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
}
// modify pixels with image processing algorithm for (y=0;y<infocolor.height;y++) {
argb * line = (argb *) pixelscolor;
uint8_t * grayline = (uint8_t *) pixelsgray;
for (x=0;x<infocolor.width;x++) {
grayline[x] = 0.3 * line[x].red + 0.59 * line[x].green + 0.11*line[x].blue;
}
pixelscolor = (char *)pixelscolor + infocolor.stride;
pixelsgray = (char *) pixelsgray + infogray.stride;
}
AndroidBitmap_unlockPixels(env, bitmapcolor);
AndroidBitmap_unlockPixels(env, bitmapgray);
}
The simple name of converttogray() is expanded to a much longer JNI name B.
Arguments to the function include a color Bitmap, which is used as the input image, and a gray Bitmap, which is the resulting (or output) Bitmap for this function. The AndroidBitmapInfo structure C holds information about a Bitmap, which is obtained with a call to AndroidBitmap_getInfoE. Details of the Bitmap are logged to LogCat with the previously introduced macros. Local variables are used to gain access to the pixel data D and loop through the rows and columns of pixels with a couple of aptly named variables, x and y. If the bitmaps are in the expected format F, the function proceeds to lock down the pixel buffers G.
At this point, the function can confidently navigate through a contiguous memory block to access the pixels of the image H. This is important because most image-pro- cessing routines rely on this sort of direct memory access through pointers. This means easier inclusion of image-processing code from sources such as existing Linux
F
Check Bitmap format
G
Lock Bitmap pixels
Access image data
H
Advance through pixel buffer
I
Unlock pixels
J
code bases. The color bitmap is accessed row by row. Each color pixel is converted to a gray pixel. Pointer arithmetic I aids in the navigation through the pixel memory buf- fer. When the images have been completely processed, the pixels are unlocked J.
When the converttogray() function is complete, the calling Java code now has a grayscale version of the color image. The Java code to call this C code is shown later in this chapter; first let’s look at the routine that detects the edges, shown in the follow- ing listing. Only the new features are discussed, as there’s a great deal of similarity between the converttogray() and detectedges routines.
JNIEXPORT void JNICALL
Java_com_msi_manning_ua2efindedges_UA2EFindEdges_detectedges(
JNIEnv * env, jobject obj, jobject bitmapgray,
jobject bitmapedges) {
AndroidBitmapInfo infogray;
void* pixelsgray;
AndroidBitmapInfo infoedges;
void* pixelsedge;
int ret;
int y;
int x;
int sumX,sumY,sum;
int i,j;
int Gx[3][3];
int Gy[3][3];
uint8_t *graydata;
uint8_t *edgedata;
Gx[0][0] = -1;Gx[0][1] = 0;Gx[0][2] = 1;
Gx[1][0] = -2;Gx[1][1] = 0;Gx[1][2] = 2;
Gx[2][0] = -1;Gx[2][1] = 0;Gx[2][2] = 1;
Gy[0][0] = 1;Gy[0][1] = 2;Gy[0][2] = 1;
Gy[1][0] = 0;Gy[1][1] = 0;Gy[1][2] = 0;
Gy[2][0] = -1;Gy[2][1] = -2;Gy[2][2] = -1;
LOGI("detectedges in JNI code");
if ((ret = AndroidBitmap_getInfo(env, bitmapgray, &infogray)) < 0) { LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
return;
}
if ((ret = AndroidBitmap_getInfo(env, bitmapedges, &infoedges)) < 0) { LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
return;
}
LOGI("gray image :: width is %d; height is %d; stride is %d; format is
%d;flags is %d",infogray.width,infogray.height,infogray.stride, infogray.format,infogray.flags);
if (infogray.format != ANDROID_BITMAP_FORMAT_A_8) { Listing 19.5 detectedges routine
Input grayscale Bitmap
B
Output edges Bitmap
C
Set up masks
D
Point to pixel data
E
Set up transformations
F
LOGE("Bitmap format is not A_8 !");
return;
}
LOGI("color image :: width is %d; height is %d; stride is %d; format is
%d;flags is %d",infoedges.width,infoedges.height,infoedges.stride, infoedges.format,infoedges.flags);
if (infoedges.format != ANDROID_BITMAP_FORMAT_A_8) { LOGE("Bitmap format is not A_8 !");
return;
}
if ((ret = AndroidBitmap_lockPixels(env, bitmapgray, &pixelsgray)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
}
if ((ret = AndroidBitmap_lockPixels(env, bitmapedges, &pixelsedge)) < 0) {
LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
}
// modify pixels with image processing algorithm graydata = (uint8_t *) pixelsgray;
edgedata = (uint8_t *) pixelsedge;
for (y=0;y<=infogray.height - 1;y++) { for (x=0;x<infogray.width -1;x++) { sumX = 0;
sumY = 0;
// check boundaries
if (y==0 || y == infogray.height-1) { sum = 0;
} else if (x == 0 || x == infogray.width -1) { sum = 0;
} else {
// calc X gradient for (i=-1;i<=1;i++) { for (j=-1;j<=1;j++) {
sumX += (int) ( (*(graydata + x + i + (y + j) * infogray.stride)) * Gx[i+1][j+1]);
} }
// calc Y gradient for (i=-1;i<=1;i++) { for (j=-1;j<=1;j++) {
sumY += (int) ( (*(graydata + x + i + (y + j) * infogray.stride)) * Gy[i+1][j+1]);
} }
sum = abs(sumX) + abs(sumY);
}
if (sum>255) sum = 255;
if (sum<0) sum = 0;
*(edgedata + x + y*infogray.width) = 255 - (uint8_t) sum;
}
Access pixels
G
Ignore border pixels
H