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.

If required, you can develop your JNI library in languages other than C/C++, although you will have to do your own translation of entry points and arguments. In general, this is not recommended. You want your extension to be as small and independent as possible. Higher-level languages may require that various libraries be installed, which can complicate deployment considerably. Threading issues add even more difficulties.

Now that we have our prototypes to provide a skeleton, we need to fill in the meat. We'll be using Visual C++ 6.0 for this example, although any Win32 compiler with the system headers should work equally well. We will only be using Win32 API calls, and no features specific to Visual C++.

The meat

We'll start by creating an empty Win32 project and workspace, making sure that it has the JDK's include folder in its inclusion path. Because we want our application to work on Windows 95, we will use the ANSI (as opposed to Unicode) versions of Win32 calls. Fortunately, JNI has the facilities needed to do the transformations.

(We're going to jump into some advanced JNI work here, so I won't cover the basics. If you find yourself getting lost, please refer to the Resources section below for more information on JNI.)

Each DesktopIndicatorHandler instance will need its own invisible window that will receive events and send them up to the Java instance, which, in turn, will delegate to its listeners.

We must consider carefully how to handle threading in our library, so let's look at the Win32 event model. Messages are not sent directly to windows, but put on an event queue owned by a thread. The thread must occasionally check for messages on its queue, and choose to either deal with them or delegate them to other callbacks. Each class of windows owned by the thread has its own callback that handles events according to the class behavior.

Although we synchronized these methods in Java, and can be sure that no more than one thread will call us before we leave our callbacks, we are still being called in arbitrary threads, meaning that each call may come from a different context. We cannot be sure that the invisible windows created in these contexts will get messages, because we cannot be sure that these threads (which are owned by the VM process) check their Win32 message queues and delegate messages. In this case, in fact, they don't. Because of this, we will create our own thread, with our own message queue handling, and make sure all our windows are created within its context. It is simple to asynchronously delegate work to our worker thread by posting custom messages to its message queue.

Creating our own native thread raises a problem, though. We will need to call Java methods from within our event-handling routine, and these will happen in the context of our own thread. The JVM, however, has its own multithreading scheme, and we cannot just intrude whenever we please. We need to have our own thread work politely with the JVM. Fortunately, JNI provides the facility for attaching our native threads to the VM and enabling them to work with the VM's synchronization scheme. When attaching the thread, we receive our own environment interface, through which we can make calls to the VM.

This is a delicate point, so we will illustrate it by including the full thread procedure:

DWORD WINAPI DesktopIndicatorThread::ThreadProc( LPVOID lpParameter )
    DesktopIndicatorThread *l_this = (DesktopIndicatorThread *) lpParameter;
    // Attach the thread to the VM
    l_this->m_vm->AttachCurrentThread( (void**) &l_this->m_env, NULL );
    MSG msg;
    while( GetMessage( &msg, NULL, 0, 0 ) )
        if( msg.message == WM_DESKTOPINDICATOR )
            // Extract handler
            DesktopIndicatorHandler *l_handler = (DesktopIndicatorHandler*) msg.lParam;
            switch( msg.wParam )
            case DesktopIndicatorHandler::enableCode:
            case DesktopIndicatorHandler::updateCode:
            case DesktopIndicatorHandler::disableCode:
                // Destroy it!
                delete l_handler;
                // No more handlers?
                if( !--l_this->m_handlerCount )
                    l_this->m_thread = 0;
                    // Detach thread from VM
                    // Time to die
                    ExitThread( 0 );
            TranslateMessage( &msg );
            DispatchMessage( &msg );
    // Detach thread from VM
    return 0;

The thread begins by attaching itself to the JVM; the thread's environment pointer is stored in its instance. Note that this is a static function, so we are passing the this pointer by ourselves via the thread's private general-purpose parameter, which we sent in our CreateThread call.

Next, we will loop on our message queue. The GetMessage function blocks until a message arrives on the queue. We are using our own user-defined message code, with our custom identifier codes and a pointer to the relevant handler instance. The handler class does the real work, so we will simply delegate the operation to its methods in our Java-safe context.

The TranslateMessage and DispatchMessage calls will delegate messages to our invisible windows. This is important to note, because we are using our own user-defined messages, and must make sure that they have different codes; if we fail to take this precaution, our thread procedure will not be able to tell the difference.

To make the code cleaner, we will make the PostThreadMessage calls implicit. For example:

void DesktopIndicatorHandler::enable( JNIEnv *env )
    g_DesktopIndicatorThread.MakeSureThreadIsUp( env );
    while( !PostThreadMessage( g_DesktopIndicatorThread, WM_DESKTOPINDICATOR, enableCode, (LPARAM) this ) )
        Sleep( 0 );

As seen in our thread procedure above, the message will cause doEnable to be called in the safe context. The weird loop and sleep setup is there because it may take a short while for the thread's message queue to initialize. Once the queue is up, PostThreadMessage should always return true. (Note that, although doEnable is private, DesktopIndicatorThread can call it because it is declared as a friend class. Yes, it's obvious, but it also may be a bit confusing with all this switching between Java and C++.)

The rest of the work is really straightforward Win32 programming. We register our window class:

// Register window class
l_Class.cbSize = sizeof( l_Class );
l_Class.style = 0;
l_Class.lpszClassName = TEXT( "DesktopIndicatorHandlerClass" );
l_Class.lpfnWndProc = WndProc;
l_Class.hbrBackground = NULL;
l_Class.hCursor = NULL;
l_Class.hIcon = NULL;
l_Class.hIconSm = NULL;
l_Class.lpszMenuName = NULL;
l_Class.cbClsExtra = 0;
l_Class.cbWndExtra = 0;
if( !RegisterClassEx( &l_Class ) )

We then create our invisible window (there's one for each handler instance):

// Create window
m_window = CreateWindow
    TEXT( "DesktopIndicatorHandlerClass" ),
    TEXT( "DesktopIndicatorHandler" ),
    0, 0, 0, 0,
if( !m_window )
// Set this pointer
SetWindowLong( m_window, GWL_USERDATA, (LONG) this );

Note that we store our this pointer in the user area on the window. We do this in order to solve the same problem we had with the thread procedure -- our window procedure is also a static function.

We can now -- finally! -- create our taskbar icon:

// Add shell icon
m_iconData.cbSize = sizeof( m_iconData );
m_iconData.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
m_iconData.uCallbackMessage = WM_DESKTOPINDICATOR_CLICK;
m_iconData.uID = 0;
m_iconData.hWnd = m_window;
m_iconData.hIcon = m_icon;
strcpy( m_iconData.szTip, m_tooltip );
Shell_NotifyIcon( NIM_ADD, &m_iconData );

We are using our own user-defined message for the callback, which is different from the message for the thread procedure, because they are both interpreted in the same place.

Our window procedure is simple:

LRESULT CALLBACK DesktopIndicatorHandler::WndProc( HWND hWnd, UINT uMessage, WPARAM wParam, LPARAM lParam )
    // Check for our special notification message
    if( ( uMessage == WM_DESKTOPINDICATOR_CLICK ) && ( INT(lParam) == WM_LBUTTONDOWN ) )
        DesktopIndicatorHandler *l_this = (DesktopIndicatorHandler *) GetWindowLong( hWnd, GWL_USERDATA );
        // Click!
        return 0;
        return DefWindowProc( hWnd, uMessage, wParam, lParam );

It extracts the this pointer and fires a click. The fireClicked method delegates upward to the Java wrapper instance via JNI:

void DesktopIndicatorHandler::fireClicked()
    g_DesktopIndicatorThread.m_env->CallVoidMethod( m_object, m_fireClicked );

Note that we are doing the call through the thread's environment, because that is the thread context in which we are running.

If you remember your JNI, you know that nonstatic native methods are called with a jobject parameter. But how did we match the jobject, which is a Java instance, with our C++ handler instance? Cast your mind back to that handler field in the wrapper ... I'm sure it's all coming back to you now, right? Here's the static C++ method to extract our pointer from the Java object:

DesktopIndicatorHandler *DesktopIndicatorHandler::extract( JNIEnv *env, jobject object )
    // Get field ID
    jfieldID l_handlerId = env->GetFieldID( env->GetObjectClass( object ), "handler", "I" );
    // Get field
    DesktopIndicatorHandler *l_handler = (DesktopIndicatorHandler *) env->GetIntField( object, l_handlerId );
    return l_handler;

You may feel apprehensive about storing C++ pointers in Java instances. Don't worry; Java will not garbage-collect your C++ objects. This is, in fact, a standard technique for making objects opaque. As far as Java is concerned, the pointer is just a number stored in a field. In this case, Java makes no use of this number, and without knowing what it means, it's just data. In some cases, these numbers may be stored in Java and returned to native code by other native method calls. In such cases, the number is called a handle.

There's one last trick concerning this: we're storing the jobject in our handler instance, and using it to call the fireClicked method. This is dangerous, because it may be readily garbage-collected, in which case terrible things will happen if we reference it. It's easy to forget this, because we never worry about reference counting in Java. But we're not in Java, and must make sure that a reference is counted for us:

// Reference object
m_object = env->NewGlobalRef( object );

We also must be careful to release the reference when it's no longer needed, or else the Java instance will never be garbage-collected:

// Release our reference
g_DesktopIndicatorThread.m_env->DeleteGlobalRef( m_object );

The full source code is available online. See Resources for the URL.

Here is a small Java application to test the feature. Make sure the DLL and the ICO files are in your path:

public class DesktopIndicatorTest implements DesktopIndicatorListener
    static DesktopIndicatorTest listener;
    static int clicks = 4;
    static int quickImage;
    static int comicImage;
    static public void main( String args[] )
        // Initialize JNI extension
        if( !DesktopIndicator.initialize() )
            System.err.println( "Either you are not on Windows, or there is a problem with the DesktopIndicator library!" );
        // Load quick image
        quickImage = DesktopIndicator.loadImage( "quick.ico" );
        if( quickImage == -1 )
            System.err.println( "Could not load the image file \"quick.ico\"!" );
        // Load comic image
        comicImage = DesktopIndicator.loadImage( "comic.ico" );
        if( comicImage == -1 )
            System.err.println( "Could not load the image file \"comic.ico\"!" );
        // Create the indicator
        DesktopIndicator indicator = new DesktopIndicator( quickImage, "Quick! Click me!" );
        listener = new DesktopIndicatorTest();
        indicator.addDesktopIndicatorListener( listener );
        // Instructions
        System.err.println( "See the taskbar icon? Click it!" );
        // Wait for the bitter end
            synchronized( listener )
        catch( InterruptedException x )
        // Time to die      
        indicator.removeDesktopIndicatorListener( listener );
        System.err.println( "Goodbye!" );
    public void onDesktopIndicatorClicked( DesktopIndicator source )
        System.err.println( String.valueOf( clicks ) + " click(s) left..." );
        // Countdown to death
        if( clicks % 2 == 1 )
            // Comic!
            source.update( comicImage, "A message for you, sir!" );
            // Quick!
            source.update( quickImage, "Quick! Click me!" );
        if( clicks == 0 )
            // The end is nigh
            synchronized( listener )

(This is a very bad application, because it does not offer the same functionality on other platforms. It does, however, fail rather gracefully.)

Suggestions for improvement

The desktop indicator solution is certainly usable as is, but may be extended to offer even more functionality.

  • Many native Windows applications feature a context menu for their taskbar icons. We can use Java to display this menu, but we must know where on the screen to display it. This information can be supplied in the listener's callback method.

  • Currently, the desktop indicator only supports ICO files to render the icons; however, using JPEGs would be more interoperable with other Java packages, and it would be nice to use JPEG files located within JARs. It would not be trivial to support this feature; runtime translation of the desired JPEG to the ICO format used by Win32 would be necessary.

GUI and beyond

JNI does have a serious limitation when it comes to extending Java's GUI: there is no standard way for getting native window handles from AWT components. Each VM may implement its AWT differently, and store the window handle in a different place. Future versions of the VM may handle windows in still different ways, ways that cannot be predicted with any certainty right now. With Java 2 platform VMs just starting to get a foothold (without this feature), I don't expect to see a standard way for doing this anytime soon.

This limitation means that it is quite impossible for your JNI extensions to alter or otherwise work with AWT-created components. You can definitely create your own mini-AWT framework to handle your platform-specific windowing features, but this requires considerable work, never integrates well, and is, of course, nonportable.

While I've focused this article on a GUI extension, there are other platform-specific features that users may expect that are not part of the GUI. One example is the Windows' registry. When running on Windows, Java applications cannot normally write their settings to the registry, and, as such, they will not be saved on the user's profile. A class that uses JNI to write to the registry on Windows, and writes to a file on other platforms, gives you the best of all worlds, without compromising its persistence features. I have supplied a rather primitive solution for this on my JNI site (see Resources).

Support JNI ... carefully

I am firmly convinced that JNI extensions are not merely cool toys for Java, but are a real necessity if Java is to transcend the small world of applets and compete in the desktop-application arena. Java aficionados have hitherto downplayed JNI for an obvious reason: applications which rely on JNI features will no longer be portable, and Java will lose its main edge and become just another VM architecture. On the other hand, with careful design considerations, JNI extensions may save the day.

I am maintaining a small site for supporting public-domain open source JNI extensions similar to the one described in this article. If more people contribute, we can build a useful library of application boosters and make Java a viable platform for real-world, cross-platform applications. See Resources for the URL.

The deployment issue

True native behavior is not the only problem with Java applications. Deployment is another big issue. Of course, you can deploy a Java application just like any other application, and there are several commercial kits for cross-platform installation, many running in Java. But it's still a far cry from deploying Java applets, and millions of miles away from HTML/DHTML Web applications, where all you have to do is turn on your Web server.

Wouldn't it be great if you could just give your users a URL for your applications and have them point their VMs to it? It would be just like using a browser, but with all the benefits of a real application with real windowing. The initiative is under way, and you are welcome to join and help where you can. It's all free software under the GNU General Public License. See Resources for more details.

Tal Liron is currently a senior developer at Goman Research (http://www.goman.co.il) and its spin-off, Distributed Devices (http://www.ddevice.com). He has experience in very diverse fields, such as embedded operating system integration, Web applications, CAD, databases, RAD, distributed logic, and object-oriented infrastructure. Lately, he's become deeply involved with the most caffeinated member of the C family. He's an object enthusiast, has been in the army, and will begin studying anthropology next year. Go figure.

Learn more about this topic

  • JNI resources in JavaWorld:

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more