Gain portability and a quicker development cycle, without sacrificing performance of native GUI code
Before the arrival of JDK 1.3, you could only use JNI for non-UI work. JDK 1.3 introduces the new Java 2 AWT Native Interface (J2AWT) API that lets you use non-Java GUI components within Java programs, though in doing so you lose the portability of a pure Java solution. When using J2AWT, you must tweak the native DLL or shared library for every platform on which it is used.
Here’s an excerpt taken from one of JDK 1.3’s header files that explains the background and the reasons behind the new API:
The AWT native interface allows a native C or C++ application a means by which to access native structures in AWT. This is to facilitate moving legacy C and C++ applications to Java and to target the need … [for these applications] to do their own native rendering to canvases for performance reasons.
Up until JDK 1.3, there was no defined way for Java programs to access the handles of underlying peer GUI components. With JDK 1.3, Sun has created a standard mechanism by which developers can allow native GUI APIs and libraries to render inside a Java AWT Canvas
object. This means that there is now an official, supported way to obtain the information that supports this functionality. And when JDK 1.3 is ported to other platforms, all the ports will expose the same information — no matter what system is being used. JDK 1.3 for Windows and JDK 1.3 for Solaris are the first implementations to provide this support.
Sun is introducing this feature for several reasons. First, it allows complex legacy software with third-party dependencies to be migrated to Java before those dependencies themselves complete migration. The second reason is performance; if the native GUI code has been fine-tuned over a long period of time with huge effort, then it makes business sense to leave it unchanged.
In this article, I will introduce the concepts behind this feature. I’ll develop a sample widget step by step, which uses the Win32 API to do the rendering. Here is a snapshot of the final widget, a circular window with a smiley face.
A step-by-step overview
The first step is to define a new Java class —
MyWindow
, say — that extends
Canvas
and overrides the
paint
method. You use the
paint
method to draw operations for AWT objects, and override it using the keyword
native
. Overriding it in this way allows you to use your own native code. You must build your native code and compile it into a dynamic link library (DLL), just as you would with any other JNI application — we’ll call the library
MyWindow.DLL
in this case. The equivalent for Solaris or Linux would be a shared object/library. You also need to load this library into your Java class
MyWindow
, using a
System.loadLibrary("MyWindow")
call.
The two elements you’ll need in order to do this are MyWindow.Java
, which provides the class that extends the Canvas
class, and MyWindow.CPP
, which contains the JNI-based entry point for the paint routine. You can find MyWindow.Java
, MyWindow.CPP
, and a batch file for automating the BUILD.BAT
build in the Resources section below.
Step 1: Create the MyWindow Java class
There is one major limitation that J2AWT imposes on this process: the native code can only work on a class that extends the
java.awt.Canvas
class. This is why
MyWindow
extends
Canvas
, as shown below.
MyWindow
can be used just like any other
Canvas
-derived class in your Java application; in our example, I’ve added
MyWindow
to
Jwindow
.
import java.awt.*;
import javax.swing.*;
public class MyWindow extends Canvas {
static {
// Load the library that contains the paint code.
System.loadLibrary("MyWindow");
}
// native entry point for Painting
public native void paint(Graphics g);
public static void main( String[] argv ){
Frame f = new Frame();
f.setSize(300,400);
JWindow w = new JWindow(f);
w.setBackground(new Color(0,0,0,255));
w.getContentPane().setBackground(new Color(0,0,0,255));
w.getContentPane().add(new MyWindow());
w.setBounds(300,300,300,300);
w.setVisible(true);
}
}
Notice that you load MyWindow.DLL
in the static block. This is how your Java application gets access to the native code. (I’ll develop that native code momentarily.) Also notice that the paint
method is declared native and has no implementation; this let’s the virtual machine know that it is supposed to call the native method from the DLL loaded in the static block.
Step 2: Generate the class’s JNI header file To generate the JNI header file for the class defined above, you need to use the command javah MyWindow.class
. First, you should make sure the class file is in your CLASSPATH
. Here’s part of the generated MyWindow.h
, which shows the function declaration.
/*
* Class: MyWindow
* Method: paint
* Signature: (Ljava/awt/Graphics;)V
*/
JNIEXPORT void JNICALL Java_MyWindow_paint
(JNIEnv *, jobject, jobject);
Step 3: Develop the complete MyWindow.CPP
Here is the complete
MyWindow.CPP
, which has the native code for the paint routine needed in
MyWindow.Java
.
#include <windows.h>
#include <assert.h>
#include "jawt_md.h"
#include "MyWindow.h"
#define X(x) (int)(xLeft + (x)*xScale/100) // Scaling macros
#define Y(y) (int)(yTop + (y)*yScale/100) // so scale is 0 - 100
#define CX(x) (int)((x)*xScale/100)
#define CY(y) (int)((y)*yScale/100)
void DrawSmiley(HWND hWnd, HDC hdc);
HRGN hrgn = NULL;
JNIEXPORT void JNICALL
Java_MyWindow_paint(JNIEnv* env, jobject canvas, jobject graphics)
{
JAWT awt;
JAWT_DrawingSurface* ds;
JAWT_DrawingSurfaceInfo* dsi;
JAWT_Win32DrawingSurfaceInfo* dsi_win;
jboolean result;
jint lock;
// Get the AWT
awt.version = JAWT_VERSION_1_3;
result = JAWT_GetAWT(env, &awt);
assert(result != JNI_FALSE);
// Get the drawing surface
ds = awt.GetDrawingSurface(env, canvas);
if(ds == NULL)
return;
// Lock the drawing surface
lock = ds->Lock(ds);
assert((lock & JAWT_LOCK_ERROR) == 0);
// Get the drawing surface info
dsi = ds->GetDrawingSurfaceInfo(ds);
// Get the platform-specific drawing info
dsi_win = (JAWT_Win32DrawingSurfaceInfo*)dsi->platformInfo;
HDC hdc = dsi_win->hdc;
HWND hWnd = dsi_win->hwnd;
//////////////////////////////
// !!! DO PAINTING HERE !!! //
//////////////////////////////
if(hrgn == NULL)
{
RECT rcBounds;
GetWindowRect(hWnd,&rcBounds);
long xLeft = 0; // Use with scaling macros
long yTop = 0;
long xScale = rcBounds.right-rcBounds.left;
long yScale = rcBounds.bottom-rcBounds.top;
hrgn = CreateEllipticRgn(X(10), Y(15), X(90), Y(95));
SetWindowRgn(GetParent(hWnd),hrgn,TRUE);
InvalidateRect(hWnd,NULL,TRUE);
} else {
DrawSmiley(hWnd,hdc);
}
// Free the drawing surface info
ds->FreeDrawingSurfaceInfo(dsi);
// Unlock the drawing surface
ds->Unlock(ds);
// Free the drawing surface
awt.FreeDrawingSurface(ds);
}
void DrawSmiley(HWND hWnd, HDC hdc)
{
RECT rcBounds;
GetWindowRect(hWnd,&rcBounds);
long xLeft = 0; // Use with scaling macros
long yTop = 0;
long xScale = rcBounds.right-rcBounds.left;
long yScale = rcBounds.bottom-rcBounds.top;
// Pen width based on control size
int iPenWidth = max(CX(5), CY(5));
HBRUSH brushBlack;
HBRUSH brushYellow;
HPEN penBlack = CreatePen(PS_SOLID, iPenWidth, RGB(0x00,0x00,0x00));
// Null pen for drawing filled ellipses
HPEN penNull = CreatePen(PS_NULL, 0, (COLORREF)0);
brushBlack = CreateSolidBrush(RGB(0x00,0x00,0x00));
brushYellow = CreateSolidBrush(RGB(0xff,0xff,0x00));
HPEN pPenSave = (HPEN)SelectObject(hdc, penBlack);
HBRUSH pBrushSave = (HBRUSH)SelectObject(hdc,brushYellow);
Ellipse(hdc,X(10), Y(15), X(90), Y(95)); // Head
Arc(hdc,X(25), Y(10), X(75), Y(80), // Smile mouth
X(35), Y(70), X(65), Y(70));
SelectObject(hdc,&penNull); // No draw width
SelectObject(hdc,&brushBlack);
Ellipse(hdc,X(57), Y(35), X(65), Y(50));
Ellipse(hdc,X(35), Y(35), X(43), Y(50)); // Right eye
Ellipse(hdc,X(46), Y(50), X(54), Y(65)); // Nose
SetBkMode(hdc,TRANSPARENT); // Use ForeColor
SelectObject(hdc,pBrushSave);
SelectObject(hdc,pPenSave);
}
The key data structure here is JAWT
, which is defined in jawt.h
(included with jawt_md.h
). It provides access to all the information the native code needs be able to draw on the Java-based GUI component. The first part of the native method is boilerplate: it populates the JAWT
structure, gets a JAWT_Win32DrawingSurfaceInfo
structure, locks the surface (only one drawing engine at a time, please!), and then gets a JAWT_DrawingSurfaceInfo
structure that contains a pointer (in the platformInfo
field) to the necessary platform-specific drawing information. It also includes the bounding rectangle of the drawing surface and the current clipping region. For more information, dig into jawt.h
and jawt_md.h
(see the section entitled “Build the environment,” below).
Java_MyWindow_paint
is the entry point that the JVM will invoke for drawing the MyWindow
. The helper function DrawSmiley
does the actual rendering using Win32 calls. To include GetDrawingSurfaceInfo
in your application, use the jawt.lib
external library (see “Build the environment”).
Step 4: Edit BUILD.BAT
Edit
BUILD.BAT
before running it and set the path for your Visual C++ 6.0 and JDK 1.3 directories as shown below. The
BUILD.BAT
compiles
MyWindow.java
, generates
MyWindow.h
, and then compiles
MyWindow.CPP
into
MyWindow.DLL
.
SET DEVSTUDIO=D:Program FilesMicrosoft Visual StudioVC98
SET JDK13=D:JDK1.3
That’s it; you are ready to go. Before running the sample, make sure that MyWindow.DLL
, JDK1.3BIN
, and JDK1.3JREBIN
are in your PATH
, and that the current directory is in CLASSPATH
; this will guarantee that MyWindow.class
will load successfully. After making sure that the PATH
and CLASSPATH
are set properly, enter java MyWindow
on the command line to run the application. A RUN.BAT
batch file is included in window.zip
(see Resources) for your convenience. Edit RUN.BAT
to set the PATH
and CLASSPATH
for JDK 1.3.
Build the environment
-
Headers: New C headers for the Windows platform have been added to the JDK’s include directory. They are:
include/jawt.h.
include/win32/jawt_md.h.
According to the JavaSoft Website, these headers are not part of the Java 2 Platform’s official specification; rather, they are provided as a convenience to developers who want a standardized way to access native drawing functionality. I think this means that people who port the JDK to other platforms can choose not to expose this API.
- Libraries: A new library, named
jawt.lib
, has been added to the SDK’s library directory. As explained earlier, it has the entry points needed for including J2AWT in your application. For example, to link to theGetDrawingSurfaceInfo
entry point, you need to includejawt.lib
in your build. -
Tools: The
javah
tool is used to generate the C/C++ header file for the native functions of the Java class, and thejavac
tool is used to compile the Java source.
Conclusion
Migrating a legacy software system to Java is a nontrivial task, especially if it includes a high-performance rendering engine. The Java 2 AWT Native Interface makes it easier to migrate in stages, allowing code that is not performance sensitive to be migrated before the critical rendering code. It also makes third-party widget developers more likely to take a serious look at developing products for Java. With the Java 2 AWT Native API, you can port existing legacy GUI code and finish development more quickly, without sacrificing your investment in the performance of a key piece of native code.