Using the Java Native Interface with Delphi

Matthew Mead


Introduction

Calling Delphi from Java

Calling Java from Delphi (The Invocation API)

Summary

Resources


Introduction

You've been working in Delphi since the early days and have accrued quite a large library of code that you reuse in every project. As a software developer, this is one of your greatest assets. This codebase has been well-tested, solid, and reliable for years, and it is unlikely that you could find similar functionality elsewhere. Now, you're thinking that it might be a good idea to try your hand at Java. You've worked with Java before and realize that the time-consuming task is not going to be with the Java language itself or available tools, but with the mountain of code that exists in your libraries. Wouldn't it be great if you could use those libraries with your Java project? Well, the good news is that you can, and the technique you will use is the Java Native Interface.

The Java Native Interface (JNI) is an API that allows a Java application to interact with code written in other languages, such as C++ and Delphi. These non-Java languages are referred to as native languages because, unlike Java code, the resulting compiled code is machine-dependent. In order for a Java program to interoperate with native code, the native code must be able to compile to a shared library. Under Windows, a shared library is known as a dynamic link library or DLL.

The Java Native Interface is just that: an interface. More specifically, it is a Java API that programmers can write to, instead of writing directly to a vendor-specific virtual machine API. This has the benefit that programmers need only implement one version of their software, rather than a version for each specific VM. Also, since the JNI is part of the Java API, all implementations of the Java VM are guaranteed to support it.

The JNI supports two paradigms or models. The first model allows Java code to access native code that resides in a DLL. This portion of the JNI doesn't have an official name, so we'll just refer to it as the JNI proper. The second model allows a native application to access Java code. This second model is known as the Invocation API and using it within a native application or shared library is known as embedding the Java virtual machine.

The Java SDK that is available from Sun Microsystems Inc. includes a C/C++ header file named jni.h. It is this header file that developers require in order to write C/C++ programs that can interoperate with Java. Unfortunately, Sun does not provide an Object Pascal unit file with the SDK. However, there is an Object Pascal translation of this header file, namely, JNI.pas, that can readily be obtained from Project JEDI's API library (http://www.delphi-jedi.org/Jedi:APILIBRARY.) This JNI package contains all of the code necessary to do JNI programming with Delphi. The JNI.pas file is fairly large, weighing in at around 105 KB, and provides all of the essential data types required to communicate with Java. In addition, there are a couple of class wrappers that give the API a more Delphi-friendly feel.

Calling Delphi from Java

One of the best ways to learn a new programming technique is by example. In keeping with tradition, we present the canonical programming example that simply prints the words "Hello World!" to the display. The twist is that Java code will invoke a Delphi procedure to do the actual printing via the Delphi library routine, Writeln. After seeing the code required to make this happen, we will examine each file in detail.

For the sake of simplicity, the Java program is implemented as a text-based (console app in Delphi) application. This is not because of any limitations with the Java Native Interface. It is done to keep the Java code extremely simple so as not to get lost in the details of Java programming.

Hello World!, JNI Style

For this example, we will need to create three files: two Java files, Main.java and HelloWorld.java, and a Delphi file, HelloWorldImpl.dpr. Figure 1 shows the purpose and relationship of the three files.

FilePurpose
Main.java Java begins execution from this file's public static main function where a Java HelloWorld object is created. After the object is created, the object's displayHelloWorld method is invoked.
HelloWorld.java Defines the HelloWorld class which declares a public native method called printHelloWorld. This class is responsible for loading the Delphi DLL at runtime.
HelloWorldImpl.dpr This DLL contains an exported procedure named displayHelloWorld which prints the words Hello World! to the screen.
Figure 1: The three files used in the Hello World! example.

There are basically four steps required to execute the example program. Let's look at each step

Step 1: Create the Java Files

As you can see in Figures 2 and 3, the Java code is trivial. Again, this was done to keep the example as simple as possible and not get buried in details of the Java language.

public class Main 
{
  public static void main(String[] args)
  {
    HelloWorld hw = new HelloWorld();
    hw.displayHelloWorld();
  }
}
Figure 2: Main.java
public class HelloWorld
{
  public native void displayHelloWorld();
  static
  {
    System.loadLibrary("HelloWorldImpl");
  }
}
Figure 3: HelloWorld.java

Step 2: Compile the Java Files

For these examples, we are using the Java Development Kit from Sun Microsystems, Inc. Most of the tools in the kit are command line tools that you run from a console (DOS window) under Windows. The command line compiler is javac.exe, and this is the tool you use to compile the Java source (.java) files into Java class (.class) files. To compile the Java files, simply invoke the Java compiler on the command line with the name (including extension) of the Java file(s) you want to compile. For this example, the command is:

javac Main.java HelloWorld.java

Technically, you don't have to include HelloWorld.java in the command because the Java compiler is smart enough to notice that Main.java uses the HelloWorld class and therefore will compile it if necessary. If no errors exist in the Java files, the compiler will have created two class files named Main.class and HelloWorld.class. At this point, you have a partially functional Java program. To run it, issue the following command:

java Main

The Java Virtual Machine (VM) will load the class file Main.class and begin execution. However, because part of the program is implemented in a Delphi DLL which we haven't built yet, you will receive the following message (or something similar):

Exception in thread "main" java.lang.UnsatisfiedLinkError: no HelloWorldImpl in
java.library.path
        at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1249)
        at java.lang.Runtime.loadLibrary0(Runtime.java:470)
        at java.lang.System.loadLibrary(System.java:778)
        at HelloWorld.(HelloWorld.java:6)

In a nutshell, this message indicates that Java can't find HelloWorldImpl.DLL, which is expected since we haven't created it yet. Once we do create this DLL, this message will disappear and the program will function as expected. Let's do that now.

Step 3: Create and Build the Delphi DLL

Creating the Delphi portion of the example should be straight-forward. The .dpr file contains all of the code required. Since the example is trivial, no other .pas modules are required. For those readers that are not well-versed in creating DLLs with Delphi, we will go over the code in detail below. Figure 4 shows the complete code for the DLL. Also, don't worry about the procedure name or parameters yet. We'll discuss those, too.

library HelloWorldImpl;

uses
  JNI;

procedure Java_HelloWorld_displayHelloWorld(PEnv: PJNIEnv; Obj: JObject); stdcall;
begin
  WriteLn('Hello world!');
end;

exports
  Java_HelloWorld_displayHelloWorld;

end.
Figure 4: HelloWorldImpl.dpr

Compiling this file results in a DLL called HelloWorldImpl.DLL. We named it HelloWorldImpl to distinguish it from the Java class file HelloWorld. The extensions (.class and .DLL) will certainly prevent a filename clash, but we want to make it clear that the DLL implements some of the functionality of the Java class, HelloWorld. Hence, we added Impl to the name of the DLL to indicate this. It also makes it easier to sense the relationships between Java class files and native DLL files. This file-naming scheme is not a requirement but rather a convention.

Step 4: Execute the Program

Ok, now we can execute the program "for real" this time:

java Main
Hello world!    <------ This is displayed at the console

And that's all there is to it! Well, at least that's the view from 10,000 feet. Let's come down a little and take a closer look.

A Closer Look at the Hello World! Example

The Java Files

Main.java
This code is "straight" Java code with no knowledge that the method displayHelloWorld is a native method. This allows the implementor of HelloWorld.displayHelloWorld to implement it either as a native method, or as a Java method. The calling program will never know and will never need to be changed if the implementation of HelloWorld.displayHelloWorld changes.

HelloWorld.java
In this example, the Java class definition is trivial. There are no members or methods defined within the class. There are some key points here:

There is a tool that comes with the JDK called javah. This is a command line program that automatically generates a C/C++ header file from the native methods in a class file. That's another reason why it is important to include the native keyword. Unfortunately, there isn't a similar tool for Delphi. (Note to self: Write a javah-like tool that produces a Delphi-compatible file.)

The Delphi DLL

Now things are getting a little interesting. There are several points here that you should notice. We'll go through this code in much more detail than the Java code above. That's the main purpose of this document: to show you how to use Delphi to implement native methods to be called from Java. Line numbers have also been included so that I can easily refer to the source code. Figure 5 shows the line-numbered version of the DLL.

First of all, notice that the file that implements the native method, HelloWorldImpl.dpr, is a .dpr file: a Delphi project file. Some Delphi programmers may never have spent much time dealing with this file. This is quite natural since the Delphi IDE does a pretty good job of generating and modifying this file for you automatically. The project file is not that different from other units (.pas files.) I decided to put everything in the project file because the code to implement the method was so trivial. (You could have put the function in a unit file and then added the unit to the uses section in the project file.)

1.  library HelloWorldImpl;
2.
3.  uses
4.   JNI;
5.
6.  procedure Java_HelloWorld_displayHelloWorld(PEnv: PJNIEnv; Obj: JObject); stdcall;
7.  begin
8.    WriteLn('Hello world!');
9.  end;
10.
11. exports
12.   Java_HelloWorld_displayHelloWorld;
13. 
14. end.
Figure 5: The source code for the DLL with line numbers for easy reference.

Ok, let's look at the file in detail:

Program Execution

To execute a Java program from the console, we simply invoke java.exe and pass the name of the class file (without the .class extension) as a parameter:

java Main    <--- type this at the console

This starts the whole process of loading the Java VM, which in turn, loads Main.class and begins executing from the function main. There is really quite of bit of "magic" going on here, so let's look at the sequence of events in detail.

Run-time exceptions

There are many ways that a native DLL can cause an exception. However, the errors I show are unique to JNI programming and not programming in general. For example, dividing by zero can cause an exception, but that isn't related to JNI programming.

It's also important to realize that the compiler can't help you here by detecting potential conflicts. This is because the Delphi compiler has no knowledge of the Java code and the Java compiler has no knowledge of the Delphi code. Sure, we've mapped the types and decorated the function names, but there is no way for the compiler to check that for us. A tool like javah would help minimize the problems, but it still wouldn't be fool-proof.

Since these are Windows DLLs, exceptions are likely to cause a dialog box to be displayed (as opposed to the Java exceptions listed above that printed error messages to the screen.) Rather than describing these "cryptic" error messages, I'll just insert screen shots so you can see for yourself. (Note that the hex numbers and addresses may be different on your machine.)

Most of the exceptions generate similar error messages so it is usually difficult (if not impossible) to tell what caused the error simply by looking at the dialog box. However, I'll point out some subtle clues that can help you locate the problems.

Debugging the DLL

It is not difficult to debug your DLLs. Most modern Integrated Development Environments allow you to specify the host application: the executable program that loads the DLL at run-time. For JNI debugging, the host application is java.exe. To specify the name and location of the application to use to debug the DLL, choose Parameters... from the Run menu:

Delphi's Run Menu

This displays the Run Parameters dialog box:

Delphi's Parameters Dialog

In the Host Application edit box, enter the full path to java.exe. (Your path will probably be different.)
In the Parameters edit box, enter Main for our example.

At this point, you can set a break point on the WriteLn('Hello World!') statement in the Delphi Java_HelloWorld_displayHelloWorld procedure. Now, press F9 to run the program within the debugger. The debugger should stop on the WriteLn('Hello World!') statement.

A More Sophisticated Example

The previous example was very trivial. Although we saw that there is a significant amount of machinery taking place behind the code, the number of lines of code is minimal. In addition, you really don't have to know that much about what is going on between the Java code and the Delphi code. The main reason for this lack of required understanding is because there was no data shared between Java and Delphi. The Java code did not pass any arguments to the native code (the Delphi procedure), nor did the native code return any value to Java. This was done to keep the introduction to the Java Native Interface as simple as possible. Well, now it's time to tackle a little more complexity. To see just what is involved when sharing data between Java and native code, we first need to take a small step back. We need to understand how Java types map into Delphi types and how signatures are encoded.

Mapping Java types to Delphi types

With the exception of void, all of the types in Java map to a corresponding type in Delphi. When mapping types from one language to another, it's not the names that we are mapping; it's the size of the type. For example, a long in Java is not mapped to the Longint in Delphi; it's mapped to the Int64 type. That's because a long in Java is a 64-bit number, whereas a Delphi Longint is only 32-bits. Figure 7 describes the mappings.

To aid the programmer in remembering the type mappings, type aliases have been created in JNI.pas. Essentially, the letter 'J' is prepended to the names of the Java types. This makes it very easy to remember which Java types map to which Delphi types.

Java TypeDelphi TypeJNI Alias in DelphiDescription
booleanByteJBoolean8 bits, unsigned
byteShortintJByte8 bits, signed
charWideCharJChar16 bits, unsigned
doubleDoubleJDouble64 bits
floatSingleJFloat32 bits
intLongintJInt32 bits, signed
longInt64JLong64 bits, signed
shortSmallintJShort16 bits, signed
voidN/AvoidN/A
Figure 7: Mapping Java types to Delphi types based on their size.

Signatures

A function's signature consists of the function's name and parameters. The function's parameters are subject to these constraints:

There are several functions in the JNI that require, as a parameter, the signature of another function. The type of this parameter is a string. Figure 8 shows how Java types are encoded into characters and strings (e.g. boolean is mapped as the uppercase letter Z) and strings (e.g. Java String is mapped as the literal string Ljava/lang/String;). It then just becomes a matter of building these strings up based on the parameters and return types of Java methods.

Java TypeMapping
booleanZ
byteB
charC
doubleD
floatF
intI
longJ
voidV
objectLfully-qualified-class;
type[][type
method signature(arg-types) ret-type
Figure 8: Encoding Java types as characters and strings.

Examples:
Java MethodSignature
void f1()()V
int f2(int, long)(IJ)I
boolean f3(int[])([I)B
double f4(String, int)(Ljava/lang/String;I)D
void f5(int, String [], char)(I[Ljava/lang/String;C)V

If you recall from the Hello World example, all native functions have two hidden parameters. These parameters are not included in this signature (since these signatures refer to Java methods.) For example, a Java method that doesn't take any arguments and doesn't return a value would have the signature:

  ()V

Later, you will see how these signatures are used by Delphi when invoking Java methods.

A More Elaborate Java Example

Our first example used a very simple Java class (HelloWorld) that had only a single native method declaration, displayHelloWorld. The class had no other methods or data fields. In this second example, the Java class has both methods and data. The class also declares two native methods. In this example, we will see how to:

Passing Parameters and Returning Values

The Java class Example declares a native method:

public native String toUpperCase(String theString);

which takes a single argument of type String and returns a value of type String. The Delphi function that implements this method looks like this:

function Java_Example_toUpperCase(PEnv: PJNIEnv; Obj: JObject; theString: JString): JString; stdcall;

As you can probably guess, the toUpperCase method simply returns its String argument in all upper case characters. Figure 9 shows the complete code for the Java class, Example, and Figure 10 shows the code for ExampleImpl, the Delphi DLL which implements some methods of the Java class.

There are a few new points of interest here with respect to the Delphi function. The first thing you'll notice is that this time the Delphi method is a function instead of a procedure. This is simply because we are returning a value to the caller (the Java code.) Also, this function is defined to accept three arguments. The first two are the hidden arguments that are present in all native methods and the third parameter is the parameter that the Java code actually passed. In this case, it is a String.

In the body of the function, we have actually used one of the hidden parameters this time. The first parameter, a pointer to the Java environment, is used to construct a Delphi class that represents the Java environment. This class, TJNIEnv, is a wrapper class for the raw JNIEnv record that is defined in JNI.pas. This class wrapper gives a more Delphi-like feel to the Java environment and also provides additional methods to hide some of the complexity. For instance, there are methods that convert Java Strings to Delphi strings and vice-versa. We simply create the TJNIEnv like this:

  // Create an instance of the Java environment
Env := TJNIEnv.Create(PEnv);

Once the TJNIEnv object is created, we can then proceed to use any of the methods that it exposes. Looking in JNI.pas, you will see that there are over 200 methods exposed from TJNIEnv. We will only be discussing a handful of them here.

In the toUpperCase native function, Java has passed in a Java String, which is simply a Java object. In order to treat this Java string as a Delphi string, we must first convert it from a Java string (object) to a Delphi string. Once it is in the Delphi-native string format, we can use it as we would any Delphi string, such as passing the string to the UpperCase function:

  // Convert the Java string to a Delphi string
Str := Env.JStringToString(theString);

  // Set all characters to upper case
Str := UpperCase(Str);

Before returning the Delphi string to the Java code, we must convert it back into a Java string. Then, before the toUpperCase function returns to Java, we Free the TJNIEnv object:

  // Convert the Delphi string back to a Java string
Result := Env.StringToJString(PChar(Str));

  // Release the Java environment object
Env.Free;

Accessing Java Fields from Delphi

The Example class contains two data fields: an integer and a String. These fields serve no real purpose in the class other than to illustrate how Delphi accesses them. The Example class declares a native method, showValuesInDelphi, which demonstrates how to access these fields. Accessing a data field of a Java object requires a three-step process. Let's see those steps necessary to access the integer variable, iValue.

The first step is to get the class that is associated with the object. This is done by calling the JNI API function GetObjectClass:

  // Get the class associated with this object
Cls := Env.GetObjectClass(Obj);

You'll notice that the argument we are passing to this function is the second hidden parameter that is passed into the showValuesInDelphi procedure. This parameter is a reference to the Java object that is calling this Delphi function. It is similar to the Self object in Delphi. The second step is to locate the iValue field:

  // iValue is a non-static integer field
FID := Env.GetFieldID(Cls, 'iValue', 'I');

This API method, GetFieldID performs a "lookup" in the class. For the method to locate the field, we need to provide the class, Cls, the name of the field, iValue, and the type of the field, I. To indicate to the API method that the field is an integer, we have to encode the type in a string, as shown above in Figure 8. The third step is to get the current value from this field. That is accomplished via the GetIntField API method:

  // Get the value of the integer
iValue := Env.GetIntField(Obj, FID);

The string value, sValue is retrieved in a similar three-step fashion, with the main difference being that sValue is a Java string and iValue is an integer. After retrieving these values, the Delphi code simply prints them to the console using the Writeln procedure.

Accessing Java Methods

The Example class contains a method called showValues that is defined within the class. The purpose of this method is to display the values of its two data fields, iValue and sValue. To make this example a little more interesting, we are going to see how to call the showValues method from Delphi. To keep things simple, we are going to call this method from within the showValuesInDelphi procedure, which was created to display the Java object's values. So, what you will end up seeing displayed on the console will actually be two sets of values. The first set is displayed via Delphi's Writeln procedure, and the second set is displayed by the Java method showValues of the Example class that is being called from Delphi.

Calling a method on a Java object is also a three-step process that is similar to the way we accessed the fields. The first step is identical: you must get the class associated with the object. Since we did this step in order to access the Java fields from the showValuesInDelphi procedure, we don't have to do it again (simply because the class is same.) The second step is to find the method we wish to call. This is very similar to the way we had to lookup the data field. We use the API function GetMethodID to perform the lookup. We need to provide this function the class, Cls, the name of the method we wish to call, showValues, and the methods signature, ()V.

  // Get the ID for the 'showValues' method of the object
Mid := Env.GetMethodID(Cls, 'showValues', '()V');

The third step is the actual call. The JNI API contains functions for invoking Java methods that return values as well as for methods that don't return values. These API functions are of the form CallXXXMethod, where XXX is Byte, Long, Double, etc. for methods that return these types of values. For methods that don't return a value, you would use CallVoidMethod, which is what we used in this example:

  // Call the object's method which will display the values
Env.CallVoidMethod(Obj, Mid, []);

Also, since the showValues method didn't require any parameters, we didn't pass any values to it, as indicated by the empty brackets, []. If we needed to pass parameters, we would have placed them within the brackets.

To see the output of this example, create and compile the Java file and the Delphi file, then execute the command:

java Example      <---- type this at the console

and the following lines will be displayed:

JNI PROGRAMMING WITH DELPHI.

In Delphi code...
iValue: 123
sValue: This is a Java string.

In Java code...
iValue: 123
sValue: This is a Java string.

The first line is the result of the Delphi function toUpperCase that was called by the Java code. The next three lines are printed via the Writeln procedure that is called within showValuesInDelphi. The last three are lines printed by the Java method showValues, which is called from within the Delphi procedure, showValuesInDelphi.

public class Example
{
  public static void main(String[] args)
  {
    Example ex = new Example();
    String s = ex.toUpperCase("JNI programming with Delphi.");
    System.out.println(s);
    System.out.println();
    ex.showValuesInDelphi();
  }

  public native String toUpperCase(String theString);
  public native void showValuesInDelphi();

  static 
  {
    System.loadLibrary("ExampleImpl");
  }

  private int iValue = 123;
  private String sValue = "This is a Java string.";
  
  private void showValues()
  {
    System.out.println("In Java code...");
    System.out.println("iValue: " + iValue);
    System.out.println("sValue: " + sValue);
    System.out.println();
  }
}
Figure 9: Example.java
library ExampleImpl;

uses
  Sysutils, JNI;

{$R *.RES}

function Java_Example_toUpperCase(PEnv: PJNIEnv; Obj: JObject; theString: JString): JString; stdcall;
var
  Env: TJNIEnv;
  Str: string;
begin
    // Create an instance of the Java environment
  Env := TJNIEnv.Create(PEnv);

    // Convert the Java string to a Delphi string
  Str := Env.JStringToString(theString);

    // Set all characters to upper case
  Str := UpperCase(Str);

    // Convert the Delphi string back to a Java string
  Result := Env.StringToJString(PChar(Str));

    // Release the Java environment object
  Env.Free;
end;

procedure Java_Example_showValuesInDelphi(PEnv: PJNIEnv; Obj: JObject); stdcall;
var
  Env: TJNIEnv;
  Cls: JClass;
  FID: JFieldID;
  MID: JMethodID;
  JStr: JString;
  iValue: Integer;
  sValue: string;
begin

    // Create an instance of the Java environment
  Env := TJNIEnv.Create(PEnv);

    // Get the class associated with this object
  Cls := Env.GetObjectClass(Obj);

    // iValue is a non-static integer field
  FID := Env.GetFieldID(Cls, 'iValue', 'I');

    // Get the value of the integer
  iValue := Env.GetIntField(Obj, FID);

    // sValue is a String field
  FID := Env.GetFieldID(Cls, 'sValue', 'Ljava/lang/String;');

    // Get the String (Object) sValue.
  JStr := Env.GetObjectField(Obj, FID);

    // Convert Java string into Delphi string
  sValue := Env.JStringToString(JStr);

    // Display the values
  Writeln('In Delphi code...');
  Writeln(Format('iValue: %d', [iValue]));
  Writeln(Format('sValue: %s', [sValue]));
  Writeln;

    // Get the ID for the 'showValues' method of the object
  Mid := Env.GetMethodID(Cls, 'showValues', '()V');

    // Call the object's method which will display the values
  Env.CallVoidMethod(Obj, Mid, []);

    // Free the Java environment object
  Env.Free;
end;

exports
  Java_Example_toUpperCase,
  Java_Example_showValuesInDelphi;

begin
end.
Figure 10: ExampleImpl.dpr

Calling Java from Delphi (The Invocation API)

Up to this point, we have seen how the Java Native Interface can be used to access Delphi (native) code from within Java code. As mentioned earlier, one of the reasons that you would want to do this is to gain reuse of large amounts of proven, tested, efficient, and debugged Delphi code. This is certainly much more productive than rewriting the Delphi code in Java. But what if you wanted to do it the other way around? What if you had large amounts of proven, tested, and debugged Java code that you would like to use in your Delphi application? Again, it would be more productive to be able to reuse this code without having to rewrite it, this time, in Delphi. Can we use Java classes from within Delphi? The answer to that question is yes.

In this section, we'll look at a portion of the Java Native Interface known as the Invocation API. This API is still part of the JNI, but it is used it in a somewhat different manner. Another term you may come across when dealing with the Invocation API is the phrase embedding the Java virtual machine in an executable program or DLL. This makes the Invocation API sound daunting, but, rest assured, it's not as difficult as it may sound. Embedding in this context simply means loading the DLL that contains the runtime code for the Java Virtual Machine. You'll see that it is no different than loading any other DLL on your system.

To contrast the Invocation API with the portion of the JNI we saw earlier, let's consider how each is invoked. In the first example, the way the program was run was by typing:

java Main

at the command prompt. This caused the Java runtime (java.exe) to be executed. Since java.exe is an executable program like any other, it is merely loaded into memory by the OS and begins executing. One of the things that the program does is to locate and load the Java class Main.class. While loading this class, the Java runtime discovers that it also must load a DLL (HelloWorldImpl.DLL.) Recall this code from HelloWorld.java

System.loadLibrary("HelloWorldImpl");

This DLL is loaded and the interaction between the Delphi code and the Java code takes place as described above.

Note: The important point to understand in this scenario is that the Java runtime, java.exe, is what bootstrapped the whole process. The user thinks that she is running a Java program and may or may not be aware that there is Delphi code executing in a DLL somewhere.

Now, with the Invocation API, things are slightly different. Instead of executing java.exe at the command prompt, we will simply run a Delphi executable (let's just call it myprog.exe for now) that has the Java runtime embedded in it. Again, embedding here simply means that we load the Java runtime from the executable, instead of running it as a program typed at the command prompt. When we run myprog.exe, the OS loads the executable into memory and it starts executing. One of the first things it does (or it could do it later) is to load the Java Virtual Machine. Since the JVM is implemented in a DLL (jvm.dll), myprog.exe simply loads it as it would any other DLL it wished to link with at runtime. Our program would execute code similar to this:

JVMHandle := LoadLibrary('jvm.dll');

Once this DLL is loaded into memory, myprog.exe can make calls into it just as it would with any other DLL and the interaction between Delphi code and Java code takes place just as before.

Note: The important point to understand in this scenario is that the Delphi program, myprog.exe, is what bootstrapped the whole process. The user thinks that she is running a Delphi application and may or may not be aware that there is Java code executing in a Java VM somewhere. Of course, the code shown above implies that the Java VM, jvm.dll, is in the user's path somewhere.

Configuring Your Machine

As is the case with many of my Java experiences, setting up and configuring a compile-time and run-time environment seems to be the hardest part of Java programming. You may not have had frustrating experiences, but I have. Setting up a Java Runtime Environment, JRE, is not that difficult, though, once you know what to do. In fact, it's simply a matter of adding a couple of directories to your PATH environment variable. If you don't have a JRE, you can download one from Sun's website. For this tutorial, you can use the JAVA 2 RUNTIME ENVIRONMENT Standard Edition, which can be freely downloaded from Sun's website. If you installed the Java 2 SDK then you probably already have a JRE installed.

To configure the Java 2 JRE, there are 2 directories that need to be added to your PATH. The directories are \bin and \bin\classic, which will be in the directory where you installed the JRE. For example, if you installed the JRE into the directory C:\JRE1.2, then you would add

C:\JRE1.2\bin;C:\JRE1.2\bin\classic

to your PATH variable. The JRE (for Java 2) is implemented in the DLL named jvm.dll, which resides in \bin\classic. All of the supporting JRE DLLs reside in directories relative to this directory, so it's important that you DO NOT MOVE OR COPY THIS FILE to another location. If you do, you will encounter strange problems that will confuse you for hours (or longer!)

During my testing of the Invocation API, I came across several "weird" situations. All of them were due to the fact that multiple versions of jvm.dll were installed on my computer while I was developing this code. Some of these DLLs failed to work correctly or even at all. I don't know why. I also don't know where they came from. I have installed many, many programs on my computers and certainly some of these programs felt is was necessary to install yet-another Java runtime on my machine. My advice to you is to make sure that you have only one copy of jvm.dll in your PATH.

Hello World, revisited

Yeah, I know what you're saying, "No, not another Hello World! program..." By using the same example in all cases, it will make it easier for you to compare and contrast the different approaches. In the Hello World example above, the words Hello World! were printed to the screen by the Delphi DLL. The DLL was loaded by a Java program, which essentially boot-strapped the whole process. Now, in this example, Java will print those words to the screen. The twist is that Java will be loaded by a Delphi program, which is boot-strapping the process this time.

This Hello World! example, as in the first Hello World! example is implemented as a console application. Again, this was done to keep the code as simple as possible while introducing the Invocation API. This Java class resides in a file called HWJava.java (Figure 11) and simply contains one public method called printHello. As you can see, this is a very trivial class, which is why it is used in this example. At this point, it's important that you don't get bogged down with the details of the Java code. Even though the Delphi program (Figure 12) is a console application, the code is not as trivial, though.

public class HWJava
{
  public void printHello()
  {
    System.out.println("Hello World from Java!");
  }
}
Figure 11: HWJava.java
program HelloWorld;

{$APPTYPE CONSOLE}

uses
  SysUtils, JNI;

var
  Options: array [0..0] of JavaVMOption;
  VM_args: JavaVMInitArgs;
  JavaVM: TJavaVM;
  JNIEnv: TJNIEnv;
  Cls: JClass;
  Mid: JMethodID;
  Errcode: Integer;
  HWObject: JObject;
begin

  try
      // Create the Java VM (using a wrapper class)
    JavaVM := TJavaVM.Create;

      // Set the options for the VM
    Options[0].optionString := '-Djava.class.path=.';
    VM_args.version := JNI_VERSION_1_2;
    VM_args.options := @Options;
    VM_args.nOptions := 1;

      // Load the VM from the DLL (jvm.dll)
    Errcode := JavaVM.LoadVM(VM_args);
    if Errcode < 0 then
    begin
      WriteLn(Format('Error loading Java VM, error code = %d', [Errcode]));
      Exit;
    end;

      // Create a Java environment from the JVM's Env (another wrapper class)
    JNIEnv := TJNIEnv.Create(JavaVM.Env);

      // Find the class in the file system. This is why we added
      // the current directory to the Java classpath above.
    Cls := JNIEnv.FindClass('HWJava');
    if Cls = nil then
    begin
      WriteLn('Can''t find class: HWJava');
      Exit;
    end;

       // Get its default constructor, init, no params, no return
    Mid := JNIEnv.GetMethodID(Cls, '<init>', '()V');
    if Mid = nil then
    begin
      WriteLn('Can''t find constructor for HWJava');
      Exit;
    end;

      // Create the HWJava object
    HWObject := JNIEnv.NewObjectA(Cls, Mid, nil);
    if Mid = nil then
    begin
      WriteLn('Can''t find method: printHello');
      Exit;
    end;

      // Find the method 'printHello' within the HWJava class
    Mid := JNIEnv.GetMethodID(Cls, 'printHello', '()V');
    if Mid = nil then
    begin
      WriteLn('Can''t find method: printHello');
      Exit;
    end;

      // Call the method
    JNIEnv.CallVoidMethod(HWObject, Mid, []);

      // Release the objects
    JavaVM.Free;
    JNIEnv.Free;

  except
    on E : Exception do
      WriteLn('Error: ' + E.Message);
  end;
end.
Figure 12: HelloWorld.dpr

After compiling the Java file and Delphi file, you should open a command window (DOS prompt) in the directory where your Delphi and Java files are and execute the Delphi program:

HelloWorld              <--- type this at the command prompt

Hello world from Java!  <--- this is displayed on the screen

A Closer Look

I explained the first Hello World! example in great detail. I won't go into quite that much depth in this example, as I expect that you have already gone through the first example and understand the basic concepts that were presented. For this example, I will explain the portions that are new and pertain to the Invocation API.

You will notice that compared to the first Hello World example, considerably more code is required. This is because in the previous example, the operating system and the Java VM did a lot of the work behind-the-scenes. Now, the programmer is required to do more of the work. Also, in this example, you will notice that every call to a JNI API function is checked for failure. This is one reason why there is more code involved in this example. In the showValuesInDelphi procedure from the second example, we didn't check any of the API calls for invalid results. Again, this was done to keep the example simple. However, this example does check the results of the function calls and you would be wise to do so also.

These numbered items summarize the basic steps required to execute a method of a Java object from a Delphi application using JNI.pas:

  1. Create an instance of TJavaVM, the wrapper class around the Java Virtual Machine.
  2. Set the options that describe how to load the VM.
  3. Load the VM
  4. Create an instance of TJNIEnv, the wrapper class around the Java runtime environment.
  5. Find the Java class that implements the method you wish to invoke.
  6. Find the constructor within the Java class.
  7. Create an object of the Java class.
  8. Find the method within the Java class you wish to invoke.
  9. Call the method of the Java object.

Let's look at each of these steps in detail:

1. Create an instance of TJavaVM

    // Create the JVM (using a wrapper class)
JavaVM := TJavaVM.Create;

This is pretty straight-forward. JavaVM is declared as:

JavaVM: TJavaVM;

so the code above calls TJavaVM's constructor Create, which simply instantiates a TJavaVM object and sets JavaVM to refer to it. Note that at this point, no JVM has been loaded. The constructor simply creates the wrapper class. We must explicitly load the VM after setting options, as shown next.

2. Set the JVM options

  // Set the options for the VM
Options[0].optionString := '-Djava.class.path=.';
VM_args.version := JNI_VERSION_1_2;
VM_args.options := @Options;
VM_args.nOptions := 1;

The relevant variables are declared as:

options: array [0..0] of JavaVMOption;
VM_args: JavaVMInitArgs;

and in JNI.pas, JavaVMOption and JavaVMInitArgs are defined as:

JavaVMOption = packed record
  optionString: PAnsiChar;
  extraInfo: Pointer;
  end;

JavaVMInitArgs = packed record
  version: JInt;
  nOptions: JInt;
  options: PJavaVMOption;
  ignoreUnrecognized: JBoolean;
end;

Essentially, the variable VM_args of type JavaVMInitArgs contains the information necessary to configure the Java VM. These options are passed to the method (within the TJavaVM class) that will actually load the Java VM. The members of the record represent:

A JavaVMOption is simply a string (actually a PAnsiChar) that contains an option that is passed to the JRE much like you would use on the command line. In the example above, we created a 1-element array of JavaVMOptions because we only need to set one option. We could have been less precise in the allocation because we are explicitly telling the JavaVMInitArgs variable, VM_args, the number of options (VM_args.nOptions := 1). Unused elements are ignored if the size of the array is larger than the value of nOptions. In the example, only one option is specified:

  Options[0].optionString := '-Djava.class.path=.';

which simply includes the current directory (denoted by the dot . ) in the JRE's search path for Java classes. We include the current directory because that's where the Java class file, HWJava.class, is in relative to the Delphi program, HelloWorld.exe. This is an important option, because without it, the JRE would not be able to find the Java class. (Shortly, we will see the Delphi code that actually causes Java to find, via a FindClass method, the Java class.) The -D option is a system property that has the syntax:

  -D(name)=(value)

We will only use the option shown above to include the current directory in the JRE's search. Consult a reference on Java for other options and their meanings.

3. Load the Java VM

  // Load the VM from the DLL (jvm.dll)
Errcode := JavaVM.LoadVM(VM_args);
if Errcode < 0 then
begin
  WriteLn(Format('Error loading Java VM, error code = %d', [Errcode]));
  Exit;
end;

Again, this is pretty straight-forward. It's also a good idea to check the return value when calling the TJavaVM.LoadVM method. JNI.pas defines the possible return values as such:

JNI_OK        =  0;  // success
JNI_ERR       = -1;  // unknown error
JNI_EDETACHED = -2;  // thread detached from the VM
JNI_EVERSION  = -3;  // JNI version error
JNI_ENOMEM    = -4;  // not enough memory
JNI_EEXIST    = -5;  // VM already created
JNI_EINVAL    = -6;  // invalid arguments

Upon successfully loading the VM, the Env property of the TJavaVM class is set. Env maps to the private field FEnv, which is declared as:

FEnv: PJNIEnv;

which is a pointer to a JNIEnv that will be used in the next step.

4. Create an instance of TJNIEnv

  // Create a Java environment from the JVM's Env (another wrapper class)
JNIEnv := TJNIEnv.Create(JavaVM.Env);

The TJNIEnv class is a wrapper around the raw JNI API that we saw in the second example. By using the TJNIEnv class, we have a more Delphi-like interface to the API.

5. Find the Java class:

  // Find the class in the file system. This is why we added
  // the current directory to the Java classpath above.
Cls := JNIEnv.FindClass('HWJava');
if Cls = nil then
begin
  WriteLn('Can''t find class: HWJava');
  Exit;
end;

If the Java class HWJava is found, the variable Cls will contain a valid value, otherwise it will contain nil. Cls is declared as:

Cls: JClass;

6. Find the constructor

  // Get the default constructor, init, no params, no return
Mid := JNIEnv.GetMethodID(Cls, '<init>', '()V');
if Mid = nil then
begin
  WriteLn('Can''t find constructor for HWJava');
  Exit;
end;

Note that the name of the constructor is <init>. In Java, a constructor's name is the same as its class, but when invoking a constructor from native code, you use the name <init>. You still must specify the signature, though, because a class can have more than one constructor and we must indicate which constructor we want.

7. Create the Java object

  // Create the HWJava object
HWObject := JNIEnv.NewObjectA(Cls, Mid, nil);
if Mid = nil then
begin
  WriteLn('Can''t find method: printHello');
  Exit;
end;

8. Find the Java method

  // Find the method 'printHello' within the HWJava class
Mid := JNIEnv.GetMethodID(Cls, 'printHello', '()V');
if Mid = nil then
begin
  WriteLn('Can''t find method: printHello');
  Exit;
end;

If the Java method printHello is found, the variable Mid will contain a valid method ID, otherwise it will be nil. Mid is declared as:

Mid: JMethodID;

In the example, the GetMethodID takes 3 parameters:

  1. Cls - The JClass that was retrieved previously by the TJNIEnv.FindClass method.
  2. 'printHello' - The name of the static method in the HWJava class. Remember: Java is case-sensitive.
  3. '()V' - The signature of the printHello method. Refer to Figure 8 for a refresher.

9. Call the Java method

  // Call the static method
JNIEnv.CallVoidMethod(HWObject, Mid, []);

This is the actual call to the Java method. There are several points of interest with this code:

Well, that is pretty much how you embed the Java Virtual Machine inside a Delphi program and call Java methods. There are many variations on this, depending on the circumstances of the Java method. You will probably want to have more robust error handling, which I left out to keep the examples simple. I encourage you to try this on your own and prove to yourself that this works. Once you've got this simple example working, you can move on to more ambitious code.

Summary

In this paper, we learned that the Java Native Interface is an API that allows Java code to interoperate with native code, such as C, C++, and Delphi. We were specifically concerned with accessing the Java Native Interface from Delphi using JNI.pas. This unit defines the data types, methods, and classes that make up this interface. Over 200 JNI methods are exposed, providing a rich and standardized set of functionality that enables Java to access Delphi code and vice-versa. By using this interface, Java and Delphi code can interoperate by sharing data and methods. The first paradigm, or model, of the Java Native Interface, the JNI proper, allows Java code to access Delphi code that is implemented as a DLL. Java code can call Delphi functions and procedures with parameters of any Java type, and Delphi functions can return any Java type. This permits Java programs to use code implemented in Delphi, which essentially means that Java can be extended to include the power of Delphi. The second paradigm we saw was the Invocation API. This portion of the Java Native Interface allows a Java Virtual Machine to be embedded within a Delphi application, giving Delphi programs the ability to share Java code. This essentially means that Delphi applications can be extended to include the power of Java.

This introduction was a whirlwind tour of programming to the Java Native Interface with Delphi and only touched the surface of this powerful API. To cover all of the functionality would literally fill up a book. That book might be The Java Native Interface: Programmer's Guide and Specification by Sheng Liang. (See reference below.) If you find programming with the Java Native Interface interesting and would like to move to the next step, this book is for you. Unfortunately, it is written for C/C++ programmers. The syntax is C/C++ but the concepts apply equally well to Delphi.

Resources

JNI Specifications

Books:

Delphi on the Web:

My other JNI work: