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(); } } } }