Enhance your Java application with Java Native Interface (JNI)

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

1 2 3 Page 2
Page 2 of 3

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:
                l_this->m_handlerCount++;
                l_handler->doEnable();
                break;
            case DesktopIndicatorHandler::updateCode:
                l_handler->doUpdate();
                break;
            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
                    l_this->m_vm->DetachCurrentThread();
                    // Time to die
                    ExitThread( 0 );
                }
                break;
            }
        }
        else
        {
            TranslateMessage( &msg );
            DispatchMessage( &msg );
        }
    }
    // Detach thread from VM
    l_this->m_vm->DetachCurrentThread();
    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
WNDCLASSEX l_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 ) )
    return;

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

// Create window
m_window = CreateWindow
(
    TEXT( "DesktopIndicatorHandlerClass" ),
    TEXT( "DesktopIndicatorHandler" ),
    WS_POPUP,
    0, 0, 0, 0,
    NULL,
    NULL,
    0,
    NULL
);
if( !m_window )
    return;
// 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
NOTIFYICONDATA m_iconData;
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!
        l_this->fireClicked();
        return 0;
    }
    else
        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!" );
            return;
        }
        
        // Load quick image
        quickImage = DesktopIndicator.loadImage( "quick.ico" );
        if( quickImage == -1 )
        {
            System.err.println( "Could not load the image file \"quick.ico\"!" );
            return;
        }
        
        // Load comic image
        comicImage = DesktopIndicator.loadImage( "comic.ico" );
        if( comicImage == -1 )
        {
            System.err.println( "Could not load the image file \"comic.ico\"!" );
            return;
        }
        
        // Create the indicator
        DesktopIndicator indicator = new DesktopIndicator( quickImage, "Quick! Click me!" );
        listener = new DesktopIndicatorTest();
        indicator.addDesktopIndicatorListener( listener );
        indicator.show();
        
        // Instructions
        System.err.println( "See the taskbar icon? Click it!" );
    
        // Wait for the bitter end
        try
        {
            synchronized( listener )
            {
                listener.wait();
            }
        }
        catch( InterruptedException x )
        {
        }
        // Time to die      
        indicator.removeDesktopIndicatorListener( listener );
        indicator.hide();
        System.err.println( "Goodbye!" );
    }
    public void onDesktopIndicatorClicked( DesktopIndicator source )
    {
        System.err.println( String.valueOf( clicks ) + " click(s) left..." );
        
        // Countdown to death
        clicks--;
        
        if( clicks % 2 == 1 )
        {
            // Comic!
            source.update( comicImage, "A message for you, sir!" );
        }
        else
        {
            // Quick!
            source.update( quickImage, "Quick! Click me!" );
        }
        
        if( clicks == 0 )
        {
            // The end is nigh
            synchronized( listener )
            {
                listener.notifyAll();
            }
        }
    }
}
1 2 3 Page 2
Page 2 of 3