Using J/Direct to Call the Win32 API from Java

By Mike Pietraszak

While the pundits and purists debate the merits of lowest-common-denominator programming, many developers back on planet Earth are busy writing Windows®-based apps. And with the advent of J/Direct™, you can enjoy the liberating productivity benefits of the Java language without being weighed down by libraries that cater to even the least functional of the “pure” platforms. Now developers who choose to throw off the shackles of purity have a choice—and that choice includes the best of both the multiplatform and Windows-specific worlds.

J/Direct is a new feature of the Microsoft® Virtual Machine (VM) that allows developers to call the entire Win32® API directly. Before J/Direct, developers who wanted to access the rich functionality of Win32 had two options. They could either wrap the Win32 API calls in a custom DLL and call the wrapper using the Raw Native Interface (RNI), or use the COM features of the VM to access the subset of Win32 APIs exposed through COM interfaces. Now J/Direct provides a third, more direct approach to accessing DLL-based Win32 APIs in a way that automatically converts native pointers, structures, and types to their Java equivalents.

The simple program in Figure 1 demonstrates the J/Direct syntax. The program plays a WAV-format audio file (a format unsupported by the AWT applet.AudioClip class) using the sndPlaySound API found in the Windows Multimedia Library (WINMM.DLL). When the code is compiled by the Microsoft Compiler for Java (JVC.EXE) version 1.02.4213 or greater, the @dll.import directive is translated into special bytecode attributes in the HelloWindows class file. The VM for Java then decodes those attributes into an instruction to load the WINMM.DLL library. The sndPlaySound function can then be called like any other Java function in the HelloWindows class. So before you get started, you’ll need the VM and the Microsoft Compiler for Java, both of which are included in the Microsoft SDK for Java 2.0. The SDK can be downloaded from http://www.microsoft.com/java.

Figure 1: HelloWIndows

public class HelloWindows {

  /** @dll.import("winmm") */
  static native boolean sndPlaySound(String lpszSound, int fuSound);

  public static void main (String args[]) {
    boolean returnCode;
    int     flags     = 0;
    String  soundFile = System.getProperty("com.ms.windir") + "\\MEDIA\\The  
                        Microsoft Sound.wav";

    System.out.println("Playing '" + soundFile + "'...");
    returnCode = sndPlaySound(soundFile,flags);
  }
}

Figure 2: HelloWindowsEx (MultiPlatform)

public class HelloWindowsEx {

  /** @dll.import("winmm",entrypoint="sndPlaySound") */
  static native boolean playWAV(String lpszSound, int fuSound);

  public static void main (String args[]) {

    if (System.getProperty("os.name").startsWith("Windows")) {

      // If the com.ms.util.SystemVersionManager class is not found, 
      // catch the NoClassDefFoundError

      try {

        java.util.Properties vmProperties = 
            com.ms.util.SystemVersionManager.getVMVersion();
        int majorVersion   = (new 
            Integer(vmProperties.getProperty("MajorVersion"))).intValue();
        int minorVersion   = (new 
            Integer(vmProperties.getProperty("MinorVersion"))).intValue();
        int buildIncrement = (new 
            Integer(vmProperties.getProperty("BuildIncrement"))).intValue();
      
        if ((majorVersion >= 4) && (minorVersion >= 79) && 
            (buildIncrement >= 2164)) 
        {

          boolean returnCode;
          int     flags     = 0;
          String  soundFile = System.getProperty("com.ms.windir") + "\\MEDIA\\The 
                              Microsoft Sound.wav";

          System.out.println("Playing '" + soundFile + "'...");
          returnCode = playWAV(soundFile,flags);
        } else {

          // Not the right version of the VM

          System.out.println("J/Direct requires VM Version 4.79.2252 or higher.");
        }
      } catch (NoClassDefFoundError e) {

        System.out.println("Class " + e.getMessage() + " not found.");

        // If com/ms/util/SystemVersionManager is not on the system, 
        // then this is probably not the VM.

        if (e.getMessage().equals("com/ms/util/SystemVersionManager")) {
          System.out.println("J/Direct requires VM Version 4.79.2252 or higher.");
        }

      }
    } else {

        // This isn't Windows.

        System.out.println("J/Direct requires a Windows system.");
    }
  }
}

The Best of Both Worlds

The HelloWindows program in Figure 1 will run great, provided that the program uses the Microsoft VM version 4.79.2252 or greater that comes with Microsoft Internet Explorer 4.0 or the SDK 2.0. But in a Web-based world, developers may not have control over the VM their clients will be using to run Java programs. So it’s possible that the program will be run on a VM that doesn’t handle J/Direct calls appropriately. Developers can still use J/Direct in a way that permits Java-based programs to run successfully in both Windows-specific and heterogeneous client environments.

Figure 2 is a modified listing for the HelloWindows program in Figure 1. HelloWindowsEx performs several checks to determine the scenario under which it is being run: the environment is Windows and J/Direct is available; the environment is Windows, but the VM-specific class com.ms.util.SystemVersionManager can’t be loaded (so the VM probably isn’t the Microsoft VM); or the environment is Windows and the virtual machine is the Microsoft VM, but it’s not the correct version so there’s J/Direct support. By writing code that recognizes these gradations, you can take advantage of Windows-specific features and still deploy to platforms that do not provide J/Direct access to Win32 APIs.

Figure 3: Parameter and Return Value

BOOL boolean
BOOL[] boolean[]
BYTE byte
BYTE* byte[]
CHAR byte
CLSID* com.ms.com._Guid
double double
double* double[]
DWORD int
DWORD* int[]
float float
float* float[]
GUID* com.ms.com._Guid
IID* com.ms.com._Guid
INT int
LONG int
LPCSTR String

Note: LPCSTR/String may be used as parameters but not as return values. In OLE mode, however, String maps to LPWSTR and both are permitted as return values. The Microsoft VM frees the string by using CoTaskMemFree.

LPSTR StringBuffer
SAFEARRAY* com.ms.com.SafeArray

Note: May only be passed as parameter, not as return value.

SHORT short
UINT int
ULONG int
VARIANT* com.ms.com.Variant
VOID void

Note: void/VOID may not be used as parameters and may only be used as return values.

WORD short
WORD* short[]
__int64 long
__int64* long[]
COM interface interface

Note: Must use jactivex or similar tool to generate interface.

function pointer com.ms.dll.Callback

Note: May only be passed as parameter, not as return value.

pointer to struct Object

Note: An IUnknown* is used instead of a pointer to a struct in OLE mode.

So now you can use J/Direct to access APIs (and you can even rename them). How do you map the signature of the API you want to call (usually documented in C) to one that uses Java intrinsic types? Another good question. When J/Direct accesses a Windows DLL from Java, the Java parameters must be marshaled from Java types to native C types. For return values, the C types must be marshaled back to Java types. This conversion is handled automatically by the VM, according to the table shown in Figure 3. Note that some conversions are only done for parameters, like Strings and SafeArrays, and some are only done for return values, like void. But as for the actual @dll.import declaration in your code, you’ll have to do that C to Java (or Visual Basic® to Java) conversion yourself.

The C signature for the sndPlaySound function is:

BOOL sndPlaySound(
  LPCSTR lpszSound,
  UINT fuSound
);

So the parameter and return value translation to Java types is as follows:

C Type Java Type
BOOL becomes boolean
LPCSTR becomes String
UINT becomes int

The Java declaration for the API import statement then becomes:

/** @dll.import("winmm") */
static native boolean sndPlaySound(String lpszSound, int fuSound);

Figure 4: Parameter and Return Value

Byte byte
Double double
Integer short
Long boolean
Long int
Single float
VARIANT com.ms.com.Variant

Converting Basic to Java

If you are using a reference that lists APIs with Visual Basic types, they can also be converted easily to Java. Figure 4 shows a table with the necessary type mappings. The sndPlaySound API would be declared in Visual Basic as:

Declare Function sndPlaySound Lib "winmm.dll" Alias "sndPlaySound" (ByVal lpszSoundName As String, ByVal uFlags As Long) As Long

The parameter and return value translation to Java types from Visual Basic is as follows:

Visual Basic Type Java Type
Long (return value) becomes int
String becomes String
Long becomes int

The return value for the C API reference was of type BOOL, and was converted to Java type boolean. For the Visual Basic API reference, the return value was type Long, which can be converted to either the Java type boolean or the Java type int. In this case, either type will work just fine. So the Java declaration derived from an API reference using Visual Basic types could be correctly translated as either

/** @dll.import("winmm") */
static native boolean sndPlaySound(String lpszSound, int fuSound);

or

/** @dll.import("winmm") */
static native int sndPlaySound(String lpszSound, int fuSound);

J/Direct also provides a flexible way for developers to map a somewhat cryptic Win32 API to a more Java-friendly name. The sndPlaySound function, for example, can be mapped (or “aliased”) to playWAV by modifying the following lines of code in the HelloWindows program:

/** @dll.import("winmm") */
 static native boolean sndPlaySound (String lpszSound, int fuSound);

/** @dll.import("winmm", entrypoint="sndPlaySound") */
 static native boolean playWAV (String lpszSound, int fuSound);

Because J/Direct calls native APIs, the code must be authenticated and approved with maximum trust in order to be run from a browser. For J/Direct code to be run from an applet, it must be digitally signed, indicating full trust and granting all permissions. Untrusted applet code cannot access trusted applet classes that use J/Direct. Even after the applet has been signed, the init, start, stay, and destroy methods of the applet must include a call to com.ms.security.PolicyEngine.assertPermission. Tools like CABARC.EXE and SIGNCODE.EXE in the Microsoft SDK for Java 2.0 provide a means for packaging and digitally signing applets with the high levels of trust needed to be run from the browser.

Applets with J/Direct

For security reasons, applets that make J/Direct calls must be packaged into a CAB and authenticated. Figure 5 shows an applet that makes J/Direct calls, and the following HTML hosts the applet:

<html>
  <applet code="HelloIE" width=300 height=50>
    <param name="cabbase" value="hello.cab">
  </applet>
</html>

Figure 6 is a .bat file that can be used to create the test certificate, shown in Figure 7, that will be displayed the first time the applet is loaded. For information on how to apply for an official certificate for commercial applications, visit http://www.microsoft.com/workshop/prog/security/authcode/codesign-f.htm.

You may find the edit-compile-debug cycle for J/Direct applets tricky because applet classes remain cached while Internet Explorer is running. This means that even if you recompile a class and redeploy it in a CAB, the old class will still be in the cache, so it will be run again and your changes will not be displayed. You can use Ctrl-F5 to force a reload of your classes, or you can restart Internet Explorer. Next, you must be sure to sign your code with low trust (signcode.exe -jp low) and request security permissions (with PolicyEngine.assertPermission). In this example, maximum permissions (SYSTEM) were requested. Finally, you’ll have to turn on test certificates by running the SETREG.EXE tool.

Figure 5: Applet Using J/Direct

import com.ms.security.*;

public class HelloIE extends com.ms.ui.UIApplet {
  /** @dll.import("winmm") */
  static native boolean sndPlaySound(String lpszSound, int fuSound);

  public String soundFile = new String();

  public void init() {
    PolicyEngine.assertPermission(PermissionID.SYSTEM);
    soundFile  = System.getProperty("com.ms.windir") + "\\MEDIA\\The Microsoft 
                 Sound.wav";
    String  buttonText = "Play '" + soundFile + "'";

    setLayout(new com.ms.ui.UIBorderLayout(0, 0));
    add(new com.ms.ui.UIPushButton(buttonText), "Center");
  }

  public boolean action(java.awt.Event e, Object arg) {
    if ( arg instanceof com.ms.ui.UIPushButton ) {
      PolicyEngine.assertPermission(PermissionID.SYSTEM);
      boolean returnCode = sndPlaySound(soundFile,0);
    }
    return true;
  }
}

Figure 6: Batch File for Signing CAB

rem //-------------------------
rem // Enable test certificates
rem //-------------------------
setreg.exe 1 true
rem
rem //-------------------------
rem // Compile HelloIE
rem //-------------------------
jvc.exe HelloIE.java
rem
rem //-------------------------
rem // Turn .class into .cab
rem //-------------------------
cabarc.exe -s 6144 n hello.cab HelloIE.class
rem
rem //-------------------------
rem // Make a test certificate
rem //-------------------------
makecert.exe -sv hello.key -n "CN=My Publisher Name" hello.cer
cert2spc.exe hello.cer hello.spc
rem
rem //-------------------------
rem // Digitally sign .cab
rem //-------------------------
signcode.exe -j javasign.dll -jp low -spc hello.spc -v hello.key hello.cab
rem
rem //-------------------------
rem // Launch the web page
rem //-------------------------
start hello.htm

Figure 7: Test Certificate

What about COM and RNI?

J/Direct is a new way for users of the Microsoft VM for Java to take advantage of Win32 APIs in Windows 95, Windows NT®, and beyond. But what’s so new about Windows access? After all, VM users can already access COM through a host of tools like the Visual J++™ Type Library Wizard for Java (JAVATLB.EXE), the Microsoft ActiveX™ Control Importer for Java (JACTIVEX.EXE), the Visual J++ ActiveX Wizard for Java, and the Java/COM Registration Utility (JAVAREG.EXE).

But with J/Direct, the Windows APIs are accessed with DLL calls, not via COM. And although RNI provides a great way to access DLLs, the RNI technology assumes that you started with a Java class, generated a C header file using msjavah.exe, and then added functionality to the C DLL. But Win32 DLLs aren’t callable by RNI in this way. Because Win32 API names don’t conform to RNI naming conventions, and because RNI expects data types (like strings) to be Java-format types (the Win32 API uses C-format types), RNI cannot be used to access Win32 or third-party DLLs. In other words, a single API cannot follow both the RNI conventions and the Windows conventions required by existing Windows-based apps.

J/Direct makes it easy to call DLL functions by automatically marshaling parameters from Java to native C types. Crossing the Java to C boundary also means dealing with garbage collection issues. In C, memory must be explicitly allocated and deallocated in code. But in Java, memory allocation and deallocation is done automatically by the VM.
This difference can be problematic if a Java-allocated variable gets deallocated by the VM while still in use on the C side. J/Direct helps when crossing this boundary by not garbage collecting (deallocating) during an API call, but there are limitations. If you access a DLL via J/Direct, that DLL cannot access another DLL that is accessed via RNI. And DLL functions cannot modify standard Java objects directly—structures built using the @dll.struct directive must be used.

In my next article, I’ll cover the @dll.struct directive, OLE calling conventions, ANSI versus Unicode issues, callbacks, and the com.ms.dll package.

 For related articles, see http://www.microsoft.com/java.