Enhance your Java application with Java Native Interface (JNI)

How to compete with native applications without sacrificing cross-platform benefits

So, you're going to develop a great new application. Java's cross-platform promises bring a twinkle to the eyes of your marketing people, but after a test run on a group of innocent users, you're not so sure Java can deliver the goods.

The main deficiency Java applications seem to have is that they do not look and feel like native applications. The reasons behind this are obvious: 100% Pure Java must support the lowest common denominator, but there are many platform-specific features and quirks that the users of specific platforms expect, and your application will seem to be lacking without them.

In today's competitive application arena, these features may be critically necessary to your application's marketplace success. A competitor's native application can use these features to beat your Java application every time, producing a product that's faster, looks and feels right, and works with the operating system and other native applications as expected.

The solution, of course, is to use Java Native Interface (JNI) extensions. But what will this mean for Java's cross-platform capabilities? If your application is only 60% Pure Java, it won't run on other platforms -- so why use Java at all? Admittedly, Java has other benefits: the language is easy to learn and use, the code is simple to maintain and debug, and by using it you leave open the possibility of migrating to other platforms in the future -- if you design your application with such migration in mind.

So, is JNI a compromise? It doesn't have to be. You can have a 100% Pure Java application in which JNI features are added benefits that appear only on specific platforms. There will be no loss for those who use the platforms not supported by your JNI features, and you will compete with native applications on equal terms where needed. The trick is to design an application that can take advantage of JNI features, but is still fully operational without them.

(Although Microsoft's virtual machine does not support JNI, it has many Windows-specific packages of features that give the same result: powerful native support. Unlike JNI extensions, however, you cannot replace these features with an alternate JNI library for an additional platform. If you use these Windows-specific features, you're committed to Windows and Microsoft's VM.)

Bonus features

Start out with a 100% Pure Java application design. Think of the application as being contained entirely within the virtual machine, which is otherwise platform neutral. All your features must be based on whatever the Java platform has to offer. When you're finished, congratulate yourself! Your design will work on any Java Virtual Machine (JVM) running on any platform.

Next, look at your Java application from a user's point of view. It's probably not the only application running in his or her environment. What features is your application lacking that its native neighbors have? Run a few native applications together with yours, and ask yourself some questions about the differences:

  • Do the native apps have special widgets that your application lacks? Tool tips are common, and so are context menus, but what about collapsing/expanding, minimizing/maximizing, and showing/hiding? Some GUIs have special popup messages and automatic behaviors. There's really no limit to what some people think users want.

  • Do both types of app handle special mouse and key combinations in the same way? Java makes no special assumptions about the number of mouse buttons, but many platforms do. Keyboards are largely standardized, but there are exceptions; some keyboards (and some platforms) have more specialized keys than others. Native applications may exhibit special operating system behavior for special key combinations.

  • Can your application interoperate with the natives? Can you drag and drop between them? What types of objects does each accept? Dragging files is common, but are other objects commonly dragged in or pasted?

  • How does your application interact with the desktop? Some platforms have very cluttered desktops, supporting taskbars, flashing messages, context menus, and even Web content. Do the native applications use these features? (This article will handle the taskbar icon feature in Windows, with which you can add application-defined icons to the desktop taskbar that indicate status without requiring an open window.)

  • How does your application interact with the operating system, as compared to the native applications? Do they handle special OS events, such as hibernation and input methods, in similar ways? Do they converse with system databases or registries? Do they write to a system event log?

Look at your list of missing features, and don't underestimate its importance. You may not like these features, finding them weird and difficult to understand, but that's just because you're not used to that platform. The average user knows them, uses them, and will expect to find them in your application. Unless your application provides functionality that is not available elsewhere, or is extremely cheap, users will always prefer native options.

But don't go overboard. Some features are so obscure or unpopular that their absence won't be noticed. Filter such features out, and list only the essentials. Try to think of these as features you want and not just obstacles to overcome. Thinking positively always helps.

The most important thing is to treat these features as an added benefit (or an annoying redundancy, if you prefer) for users of a specific platform, but never as features essential to the functioning of the application. Your app should be entirely and fully usable on either the Standard or Micro edition of the Java platform. This is the only way to protect the application's cross-platform portability -- and considering that portability was probably why you chose Java as a platform in the first place, it makes sense to protect it.

Talking the talk

In our exercise above, you probably came up with a list of features your application lacked when compared to native applications running in the same environment. You may be surprised by how many you can implement purely in standard Java:

  • The Java 2 platform has an improved Abstract Windowing Toolkit (AWT), and contains the Java Foundation Classes (JFC); the latter has a large set of lightweight, pure Java components capable of mimicking native platforms. The Pluggable Look and Feel (PLAF) architecture even lets you mimic an alternate look-and-feel architecture on any platform (within licensing limits) and customize your own. It is important to remember that the simulacrum is never the same as the original, and that there may be small quirks that the simulated PLAFs do not cover. As of JDK 1.2.2, Swing (JFC's GUI component set) is not very stable, but has a very flexible design and a bright future. JDK 1.3 (code-named Kestrel, and now available in beta) promises to be more stable and even more feature rich.

  • JFC also supports the dragging and dropping of objects between JVMs, and between JVMs and native applications. Again, there are various quirks that need attention. Don't claim it works until you've tested it on every platform.

  • The Java 2 platform also introduces standard extensions to the platform that may cover your needs. The Java Sound API and the Java 3D API are implemented with JNI. It is always best to use standard extensions, which are guaranteed to be widely supported on many platforms.

If you do decide to use JNI for your solution, don't immediately go native; keep the Java platform-neutral aloofness in mind. Consider each feature as if it were part of your virtual machine. Give it a name that is as generic as possible. Design the classes to be similar to those in the Java platform packages. Such aloofness is useful because there may be ways to simulate this feature on other platforms; when the time comes to do so, using platform-specific names and behavior models may seem out of place.

In this article, we will call our feature a desktop indicator, and will treat it with all the common Java courtesies, such as decorating it with listener classes. It's actually a fairly portable feature, and is available in some form or another on most desktop platforms.

Designing the wrapper

We'll start by designing our Java wrapper from a non-native perspective. Under Win32, the taskbar icon works by sending messages to windows; this is necessary because of Win32's reliance on messaging to handle events. In Java, we prefer to use listener classes. Part of our implementation challenge will be to provide a native invisible window that receives the events and dispatches them to listeners. This design is much more flexible than one based on the Win32 model. In taking this route, we have overcome the temptation to send events to AWT windows, and enabled our feature to work without any AWT involvement.

Let's start with the listener:

public interface DesktopIndicatorListener
    * Called when a desktop indicator is clicked.
    void onDesktopIndicatorClicked( DesktopIndicator source );

That's pretty straightforward.

As for our class, we will associate each instance with exactly one taskbar icon. We will need one static method to ensure that the library is loaded, and to indicate that the feature is supported on this VM:

/** * Loads the JNI library, if available. *

* * Must be called before images are loaded and instances are created. **/ public static boolean initialize() { // Load JNI library try { System.loadLibrary( "DesktopIndicator" ); } catch( UnsatisfiedLinkError x ) { return false; } return true; }

Note that we are catching unsatisfied link exceptions because we want to deal gracefully with platforms that do not support this feature.

As for images, it makes sense to load them internally. Operating systems usually have internal structures for handling image lists that are far more efficient than loading them from a file. If we want to have a lot of blinking and icon changes, we will want the images loaded and cached before use. To make things simpler, we'll have one global image list for all instances, so this, too, will be a static method:

/** * Loads an image file to memory. *

* * The image is loaded from the filename parameter, while the tool tip is * supplied in the tool tip parameter. The image file must be in a format * recognized by the native platform (for example, a ICO file for Win32). *

* * A return value of -1 indicates failure. Otherwise, it is the handle of * the image to be used for desktop indicators. **/ public static int loadImage( String filename ) { try { return nativeLoadImage( filename ); } catch( UnsatisfiedLinkError x ) { return -1; } }

This method is a wrapper for a private native method that adds graceful handling of exceptions. A good convention for this kind of situation is to give the native version a native prefix, because otherwise these methods are identical:

private synchronized static native int nativeLoadImage( String filename ) throws UnsatisfiedLinkError;

Note that we are synchronizing the native method, rather than doing our own internal synchronization in the native code. The latter procedure is potentially tricky, since native synchronization services do not necessarily have anything to do with those of a JVM; a JVM implements its own threads. We'll go further into threads when we get into the native code.

The other methods are quite obvious. They handle the showing and hiding of the indicator, and the updating of the image and tool tip. We will add one special private field:

private int handler = 0;

This field is not used by the Java code at all, and is meant as a placeholder for the native code to keep a link to an equivalent underlying native class, called DesktopIndicatorHandler.

We now have our wrapper, and the design seems generic enough to be ported to other platforms. Let's go native!

Walking the walk

Our native methods must somehow be converted to native code entry points. Javah, an integral tool of the JDK, generates a standard C/C++ header file from our Java source. Let's run Javah on our wrapper class:

> javah DesktopIndicator

Our C/C++ header starts with this:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include < jni.h >
/* Header for class DesktopIndicator */
#ifndef _Included_DesktopIndicator
#define _Included_DesktopIndicator
#ifdef __cplusplus

The header file includes jni.h, so it must be used in conjunction with the other C header files in the JDK that define various types and classes used.

The header file continues with prototypes of our entry point functions that are directly equivalent to the native Java methods:

extern "C" {
 * Class:     DesktopIndicator
 * Method:    nativeDisable
 * Signature: ()V
JNIEXPORT void JNICALL Java_DesktopIndicator_nativeDisable
  (JNIEnv *, jobject);

Although the functions assume C-calling conventions, the header files are C++-aware, and are easier to work with in C++, which, like Java, is an object-oriented member of the C family. We will use C++ for this example.

1 2 3 Page 1
Page 1 of 3