Using the Java Native Interface with Delphi
(Part Three - The Invocation API)

Matthew Mead

Home Page (on the Web) - News and Updates

Part One - Introduction and Tutorial
Part Two - Techniques and Examples
Part Three - The Invocation API

Contents

OverviewWhat's the Invocation API?
ConfigurationSetting the proper path for the Invocation API.
ExampleYet another Hello World! example.
A Closer LookThe Hello World! example in detail.
Alternate JRE VersionThe Hello World! example using the JRE in the JDK 1.1.5.
Dynamic LoadingChoosing the JRE and version at runtime.
A GUI Example This example shows a Delphi GUI application that uses a Java class to download web pages to your computer.
  1. The Java Code - A Java class called PageGrabber implements the functionality of downloading HTML pages.
  2. The Delphi Application - A Delphi program called URLGrabber implements the UI for application.
  3. Loading the VM - Demonstrates loading the Java VM and storing the Java environment in a member of the Delphi class.
  4. Class Method - Demonstrates calling a Java class (static) method from the application's main thread.
  5. Object Method - Demonstrates calling a Java object (non-static) method from the application's main thread.
  6. Threads - Demonstrates calling a Java method from a worker thread.
  7. Multiple Threads - Demonstrates calling a Java method from multiple threads.
SummaryWhat's next?
Overview
This part of Using the Java Native Interface with Delphi is concerned with the Invocation API. This API is still part of the JNI, but it is uses 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.

Up to this point, our study of the Java Native Interface has focused primarily on accessing Delphi (native) code from within Java code. There are many reasons why you would want to do this, and one of the biggest reasons for doing this is to gain reuse of the millions of lines 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 millions of lines 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. Well, that's one of the big features of the Invocation API.

To contrast the Invocation API with the portion of the JNI we have been learning up until now, let's consider how each is invoked. In all of the preceding tutorial and examples, 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 (in our tutorial from Part One, this is 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 in Part One and Part Two of these pages.

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 we've seen in all of our previous examples.

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. Also note that jvm.dll is the DLL that contains the Java VM for version 1.2 of the JDK. The VM for version 1.1 is located in javai.dll. We will see how to detect and use both versions later in this document.

The Invocation API portion of the Java Native Interface translation is still a work in progress. Although the translation is complete, the functionality of the wrapper class, TJavaVM is not. This doesn't mean that the wrapper class cannot be used, it simple means that it is not complete. There are several more "features" that I'd like to add that hide the underlying API a little better. I felt that it was more useful to release the translation in this state, rather than wait (what could be weeks or months or longer) until the other features were completed. Maybe that would be in a 1.1 (or 2.0) release of this translation. Also, as is the case with most software, until I receive and compile feedback, it's difficult to decide what features are missing. So, if you think something is missing, it's because it probably is.

Back to top

Configuring Your Machine (Windows platform)
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.

Java 2 JRE

If you are working with 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 days!)

JDK 1.1 JRE

If you are working with the JDK 1.1.x runtime then there is only one directory that need to be added to your PATH. The directory is \bin, which will be in the directory where you installed the JRE. For example, if you installed the JRE into the directory C:\JRE1.1.6, then you would add
  C:\JRE1.1.6\bin
to your PATH variable. The JRE (for JDK 1.1.x) is implemented in the DLL named javai.dll, which resides in \bin. 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 days!)
Notes

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 and javai.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 and javai.dll in your PATH.

Also, because the names of the DLLs are different between JDK 1.1.x and Java 2, you can easily have both installed and accessible simultaneously. This certainly makes it easy to test and use both versions of the JRE on the same machine (if you require this ability.)

Back to top

A Hello World! Example
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 example from Part One, 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. The Delphi program is boot-strapping the process this time as described in the Overview above.

This Hello World! example, as in the first Hello World! example is implemented as a console application. This was done to keep the code as simple as possible while introducing the Invocation API. A later example will be a complete Windows GUI application.

This is the Java code that will print the words Hello World from Java! to the screen:

public class HWJava
{
  public static void printHello()
  {
    System.out.println("Hello World from Java!");
  }
}
This Java class resides in a file called HWJava.java and simply contains one public static function called printHello(). As you can see, this is a very trivial class which is why it is used in this example. The Delphi code is not going to be as trivial, though. At this point, it's important that you understand the mechanisms used by Delphi to access Java code at runtime without getting bogged down with the details of the Java code.

Here is the Delphi console application in a file called HelloWorld1.dpr:

program HelloWorld1;

{$APPTYPE CONSOLE}

uses
  SysUtils, JNI;

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

  try

      // Create the JVM (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
    Errcode := JavaVM.LoadVM(VM_args);
    if Errcode < 0 then
    begin
      WriteLn(Format('Error loading JavaVM, 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;

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

      // Call the static method
    JNIEnv.CallStaticVoidMethod(Cls, Mid, []);

  except
    on E : Exception do
      WriteLn('Error: ' + E.Message);
  end;
end.
You should open a command window (DOS prompt) in the directory where your Delphi and Java files are and execute the Delphi program:
HelloWorld1             <--- Type this at the command prompt

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

Back to top

A Closer Look at the Hello World! Example
In Part One, I explained the Hello World! example in great detail. I won't go into 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.

These are the basic steps required to execute a method of a Java class 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 method within the Java class.
  7. Call the method
Let's look at each of these steps in detail:

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 the TJavaVM's constructor Create, which simple 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.

Setting 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..4] 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 5-element array of JavaVMOptions, although we only used one element. We could have been more precise in the allocation, but because we are explicitly telling the JavaVMInitArgs variable, VM_args, the number of options (VM_args.nOptions := 1), the size of the array is unimportant. 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 vő4\current directory because that's where the Java class file, HWJava.class, is in relation to the Delphi program, HelloWorld1.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.
Load the Java VM:
    // Load the VM
  Errcode := JavaVM.LoadVM(VM_args);
  if errcode < 0 then
  begin
    WriteLn(Format('Error loading JavaVM, 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.
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 Part Two of this series. By using the TJNIEnv class, we have a more Delphi-like interface to the API.
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;
Find the Java method:
    // Find the static method 'printHello' within the HWJava class
  Mid := JNIEnv.GetStaticMethodID(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;
Note that since the Java method HWJava.printHello is a static method, we use the GetStaticMethodID function, rather than the GetMethodID function. Refer to Concept #4 from Part Two of the tutorial for an example of the GetMethodID function. In the example, the GetStaticMethodID 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 signatures in Part Two for a refresher.
Call the Java method:
    // Call the static method
  JNIEnv.CallStaticVoidMethod(Cls, Mid, []);
This is the actual call to the Java method. There are several points of interest with this code: In a later example, we will see how to call methods in a Java object as well as pass parameters to the methods and return values from them.
Well, that is pretty much how you call a Java method from a Delphi program. There are many variations on this, depending on the circumstances of the Java method. Also note that you would want to call Free the TJavaVM and TJNIEnv objects within this code, but that was left out to keep the code 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.

Back to top

Using a Different JRE Version
By default, JNI.pas implicitly loads jvm.dll, the implementation of the JRE in the Java 2 platform. However, it is possible to use a different version of the JRE. Before Java 2, the JRE was implemented in javai.dll, which was part of the JDK 1.1.x distribution. For the most part, you can interchange the two JREs. Some features were added to the Java 2 JRE, but we won't explore any of those in this tutorial.

The new version is called HelloWord2.dpr and is shown below. I put comments next to the 2 new variable declarations, and also around a block of code which is different from the previous version. These differences are discussed after this code:

program HelloWorld2;

{$APPTYPE CONSOLE}

uses
  SysUtils, JNI;

var
  VM_args11: JDK1_1InitArgs; // this is different
  Classpath: string;         // this is different
  JavaVM: TJavaVM;           
  JNIEnv: TJNIEnv;
  Cls: JClass;
  Mid: JMethodID;
  Errcode: Integer;
begin

  try

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

    // ********* this is different, start *****************

        // Set the options for the VM
      Errcode := JNI_GetDefaultJavaVMInitArgs(@vm_args11);
      if Errcode < 0 then
      begin
        WriteLn(Format('JNI_GetDefaultJavaVMInitArgs failed, error code = %d', [Errcode]));
        Exit;
      end;

        // Add the current directory to the classpath
      Classpath := vm_args11.classpath + ';.';
      vm_args11.classpath := PChar(Classpath);

        // Load the VM
      Errcode := JavaVM.LoadVM(vm_args11);

    // ********* this is different, end *****************

    if Errcode < 0 then
    begin
      WriteLn(Format('Error loading JavaVM, 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;

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

      // Call the static method
    JNIEnv.CallStaticVoidMethod(Cls, Mid, []);

  except
    on E : Exception do
      WriteLn('Error: ' + E.Message);
  end;
end.
The first thing to notice is that there are two new variable declarations:
VM_args11: JDK1_1InitArgs; // this is different
Classpath: string;         // this is different
The original code used JavaVMInitArgs and JavaVMOptions. Early versions of the JRE require a variable of type JDK1_1InitArgs. The Classpath variable (a string) is simply a variable to hold a newly constructed classpath, which is required by the JDK 1.1.x.

I've added numbers to the new block of code to explain some details.

       // Set the options for the VM
 1.  Errcode := JNI_GetDefaultJavaVMInitArgs(@VM_args11);
 2.  if Errcode < 0 then
 3.  begin
 4.   WriteLn(Format('JNI_GetDefaultJavaVMInitArgs failed, error code = %d', [Errcode]));
 5.   Exit;
 6.  end;

       // Add the current directory to the classpath
 7.  Classpath := VM_args11.classpath + ';.';
 8.  VM_args11.classpath := PChar(Classpath);

       // Load the VM
10.  Errcode := JavaVM.LoadVM(VM_args11);
Details about the above code: There is one last thing that needs to be done. You must define the symbol JDK1_1 in the Project Options dialog.

This will cause the JDK 1.1-specific code in JNI.pas to become enabled. Because the compiler needs to know which version (hence, which DLL) to link into your program, this has to be done. If you look in JNI.pas, you will see this code:

    {$IFDEF JDK1_1}
    JvmModuleName = 'javai.dll';
    {$ELSE}
    JvmModuleName = 'jvm.dll';
    {$ENDIF}
Note that you must Build the project after defining a symbol in this dialog. If you just do a Compile, JNI.pas will not be recompiled and the portions that support version switching won't be enabled. You may have noticed that, so far, you haven't had to specify the names of these DLLs anywhere in your code. This is why (if you use the default static loading of the JRE) you must have javai.dll (for JDK 1.1) and/or jvm.dll (for Java 2) in your PATH. Later, we will see how to use a different DLL that's not in your PATH.

Notes:

At this time, JNI.pas only supports two versions of the JRE, JDK 1.1 (1.1) and Java 2 (1.2). Sun has release a version 1.3 of the Java 2 platform which I haven't included yet. I suspect that it will not be difficult to add this capability. The source to JNI.pas is available to you, so if you need version 1.3 support now, you should have little problems adding it.

A good way to understand what's in the value of classpath in the VM_args11 record is to print the value at runtime. For example, on my machine, when I print out this value using both versions, this is what it shows:

JDK 1.1.6 (The classpath is actually a single line. I broke it up so it would fit here.)

  n:\jre1.1.6\bin\..\classes;n:\jre1.1.6\bin\..\lib\classes.zip;
  n:\jre1.1.6\bin\..\lib\classes.jar;
  n:\jre1.1.6\bin\..\lib\rt.jar;
  n:\jre1.1.6\bin\..\lib\i18n.jar
Java 2 (The classpath is actually a single line. I broke it up so it would fit here.)
  N:\Borland\JBuilder35\jdk1.2.2\jre\lib\rt.jar;
  N:\Borland\JBuilder35\jdk1.2.2\jre\lib\i18n.jar;
  N:\Borland\JBuilder35\jdk1.2.2\jre\classes
Note that this is before adding the current directory to the classpath. Also, the CLASSPATH environment variable has no affect on the JRE's classpath.

Back to top

Choosing the JRE and Version at Runtime
In the previous section, you saw how it was possible to use different versions of the JRE. Although each example used a different version of the JRE, the version you chose to use still had to be known at compile time. In other words, both of the previous examples required that the user have jvm.dll and/or javai.dll correctly installed on their computers. But what if you didn't want to "hard code" the version (or name of the DLL) into your code? In this case, you'd need to use another method called dynamic loading or explicit loading. (The term linking is also used in place of the term loading.)

Technically, there are differences between dynamic/explicit loading and static/implicit loading. The default behavior of JNI.pas is to implicitly load the JRE. There are pros and cons to this method, and some of those are:

Explicit loading also has its pros and cons: As you can see, items that are pros in one method are cons in another, and vice-versa. There certainly are other considerations, but these are just a few of the common ones. By looking at these factors, one might conclude that explicit loading is a better way to go, since it has more pros than cons. However, if many DLL procedures will always be required in a DLL, explicitly locating them could outweigh the pros. However, in the case of JNI.pas, the wrapper class TJavaVM encapsulates the explicit loading/unloading so you, as the programmer, don't have to worry about it. This may give explicit loading another positive vote.

Using the Hello World! example from above, we will see how to modify it to use dynamic loading, instead of static (implicit) loading. The change is trivial. In fact, only two changes are required: one line of code is modified and a symbol is defined in the Project Options dialog. For brevity, I'm only going to show the line of code that differs between dynamic and static loading. Recall this line of code from the original program, HelloWorld1.dpr:

  // Create the JVM (using a wrapper class)
JavaVM := TJavaVM.Create;
To modify this to work with the dynamic loading mechanisms, the line should be changed to this:
  // Create the JVM (using a wrapper class)
JavaVM := TJavaVM.Create(JNI_VERSION_1_2, 'jvm.dll');
If you think about it, this makes sense. Since we are not using static loading (implicit linking), we must specify the necessary information. This information is the version of the JRE we are loading (JNI_VERSION_1_2) and the name of the DLL that implements the JRE (jvm.dll). Also, we must define the symbol DYNAMIC_LINKING in the Project Options dialog:

Note that you must Build the project after defining a symbol in this dialog. If you just do a Compile, JNI.pas will not be recompiled and the portions that support the dynamic mechanisms won't be enabled. That's all there is to it. Because JNI.pas does all of the work of loading/unloading the DLL and locating the necessary procedures (using the Win API function GetProcAddress), the amount of extra work required to use this method is trivial.

Notes:

It is possible to use a JRE that isn't in your PATH. To do this, you must specify the path to the JRE as in this code:
  // Create the JVM (using a wrapper class)
JavaVM := TJavaVM.Create(JNI_VERSION_1_2, 'C:\jdk1.2.2\jre\bin\classic\jvm.dll');
or, to use a JDK 1.1 version of the JRE:
  // Create the JVM (using a wrapper class)
JavaVM := TJavaVM.Create(JNI_VERSION_1_1, 'C:\jre1.1.6\bin\javai.dll');
Note that this DLL must be in the proper directory relative to other JRE support files. Refer to Configuring Your Machine above for details on installing a JRE.

Back to top

A GUI Example
All of the previous examples have focused on using Delphi console applications to demonstrate accessing the Java Native Interface. This was primarily done to keep the code as simple as possible. (Although it is arguable that the code necessary to implement simple GUI applications in Delphi is equally simple.) In the example that follows, a more realistic Delphi application will be developed that will show you how to integrate Java code into your Delphi program. This example is by far the most complex in this tutorial. However, if you've followed the tutorial up to this point, it won't seem like that big of a step. All of the previous examples have worked towards bringing you to this point. You will actually only learn one new concept in this example: How to use the Java Native Interface with Delphi threads. We will see this technique later in the example.

For this example, we will develop a Delphi application that is a complete, stand-alone, GUI application. The application is called URLGrabber and it's purpose is to download HTML pages from the Internet to your computer, not unlike how a web browser would do it. Through the UI, the user of the program must specify the URL of the web page to download (e.g. http://www.delphi-jedi.org/index.html)

The twist this time (there is always a twist <g>) is that the actually retrieving of the HTML document will be done by Java code. The name of the Java object is PageGrabber and it is implemented in a file named PageGrabber.java. The Delphi application will create this Java object and call a method, Fetch, which requires a single parameter of type string. This parameter is (you guessed it) the URL of the HTML document to download. After the Java object completes the task of retrieving the HTML document, it returns this document to the Delphi application where it will be displayed in a TMemo component on the form.

Download the files for this project.

The Java Code

From Delphi's point-of-view, there are only two methods of concern in this class. They are:
  public static String FetchS(String inputURL)
  {
    PageGrabber pg = new PageGrabber();
    return pg.Fetch(inputURL);
  }

  public String Fetch(String inputURL)
  {
    return GetHTML(inputURL);
  }

The fundamental difference between them is that the first one, FetchS is a static (class) method, which simply calls the non-static method, Fetch. This was done so that you can see the different coding that is required in Delphi to access these methods. You can consider these two methods as the public interface of the PageGrabber class. However, the actual work is done by the private GetHTML method: (I've omitted everything except the non-essential parts)
  private String GetHTML(String inputURL)
  {
    String html = "";
    String thisLine = "";
    URL url;

    try
    {
      url = new URL(inputURL);

      try
      {
        BufferedReader theData = new BufferedReader(new InputStreamReader(url.openStream()));
        try
        {
          while ((thisLine = theData.readLine()) != null)
          {
            html = html + thisLine;
          }
          // *** Omitted exception handlers ***
        }
      }
    }
  }
The full listing is in PageGrabber.java. There are a couple of points to be made here: Since the point of this exercise is to demonstrate how to access this Java code from Delphi, I'm not going to discuss the Java code. Also, there are probably a dozen other ways to accomplish this same task with Java. I have chosen to implement in the way shown above. (Actually, I wrote this several years ago and thought that it would make a good sample for this tutorial. I don't claim that it is a good example of Java coding.)

The Delphi Application

The application's main form is pictured below. This snapshot shows the HTML code for the home page of www.planetquake.com in a TMemo component. The HTML code was downloaded by the Java code and passed back to the Delphi application to display.

The main form contains eight primary UI elements:

The full source code for the main form is here: URLGrabberMainForm.pas. The project source is here: URLGrabber.dpr. All of the files for this project are zipped here: URLGrabber.zip.

Loading the Java VM

Creating the necessary Delphi objects and loading the VM for the application is pretty much the same as it was for the Hello World! example. The main difference is that we store these objects in fields of the TForm1 class. These fields are declared as:
FJavaVM: TJavaVM;
FJNIEnv: TJNIEnv;
The code is to create the objects and load the VM is in the OnClick handler for the TButton btnLoadVM and is pretty much self-explanatory:
procedure TURLGrabberForm.btnLoadVMClick(Sender: TObject);
var
  Errcode: Integer;
  VM_args11: JDK1_1InitArgs;
  VM_args: JavaVMInitArgs;
  Classpath: string;
  Options: array [0..10] of JavaVMOption;
begin
  UpdateStatusBar('Loading VM...', True);

  try
      // Create the wrapper for the VM
    FJavaVM := TJavaVM.Create;

      // Get default settings (so we can display them)
    Errcode := JNI_GetDefaultJavaVMInitArgs(@VM_args11);
    if Errcode < 0 then
    begin
      ShowMessageFmt('JNI_GetDefaultJavaVMInitArgs failed, error code = %d', [Errcode]);
      Exit;
    end;

      // Display the classpath (this is just for reference)
    Classpath := VM_args11.classpath;
    mmoHTML.Lines.Add('CLASSPATH=' + Classpath);

      // Set up the options for the VM
    FillChar(Options, SizeOf(Options), #0);
    Options[0].optionString := '-Djava.class.path=.';
    VM_args.version := JNI_VERSION_1_2;
    VM_args.options := @Options;
    VM_args.nOptions := 1;

      // Load the VM
    Errcode := FJavaVM.LoadVM(VM_args);
    if Errcode < 0 then
    begin
        // Loading the VM more than once will cause this error
      if Errcode = JNI_EEXIST then
        MessageDlg('Java VM has already been loaded. Only one VM can be loaded.', mtError, [mbOK], 0)
      else
        ShowMessageFmt('Error creating JavaVM, code = %d', [Errcode]);
      Exit;
    end;

      // Create the Env class
    FJNIEnv := TJNIEnv.Create(FJavaVM.Env);

      // Enable UI buttons
    btnCallClassMethod.Enabled := True;
    btnCallObjectMethod.Enabled := True;
    btnCallMethodFromThread.Enabled := True;
    btnCreateMultipleThreads.Enabled := True;

  except
    on E: Exception do
    begin
      ShowMessage('Error: ' + E.Message);
      UpdateStatusBar('Load VM failed.');
      Exit;
    end;
  end;

  UpdateStatusBar('Ready.');
end;

Calling a Class Method

Calling a class (static) method of a Java class is slightly simpler than calling an object (non-static) method. This is because no Java object is required. In other words, you don't have to create an object from the class before calling the method. The Java class PageGrabber has a static method called FetchS. (The 'S' at the end is to remind you that it's the static version of the Fetch method, which is also in the class.) The code to locate the class, locate the static method, and call the method, is shown below:
procedure TURLGrabberForm.btnCallClassMethodClick(Sender: TObject);
var
  Cls: JClass;
  Mid: JMethodID;
  HTML: string;
  URL: string;
  JStr: JString;
begin
  UpdateStatusBar('Calling class method...', True);

  try
      // Get the URL from the UI
    URL := lstURLs.Text;

      // Find PageGrabber class
    Cls := FJNIEnv.FindClass('PageGrabber');
    if Cls = nil then
    begin
      ShowMessage('Can''t find class: PageGrabber');
      Exit;
    end;

      // Locate static method 'FetchS' in class
    Mid := FJNIEnv.GetStaticMethodID(Cls, 'FetchS', '(Ljava/lang/String;)Ljava/lang/String;');
    if Mid = nil then
    begin
      ShowMessage('Can''t find method: FetchS');
      Exit;
    end;

      // Call the static method
    JStr := FJNIEnv.CallStaticObjectMethod(Cls, Mid, [URL]);

      // Convert the returned JString to a Delphi string
    HTML := FJNIEnv.JStringToString(JStr);

      // Display the HTML
    mmoHTML.Lines.Add(HTML);

  except
    on E: Exception do
      ShowMessage('Error: ' + E.Message);
  end;

  UpdateStatusBar('Ready.');
end;

Calling an Object Method

Calling a Java method on an object is slightly different than calling a static method. A non-static method requires an object to be constructed before it can be called. The Java class PageGrabber contains a non-static method called Fetch. In order to call this method, an instance of the PageGrabber class must first be constructed. Since this construction of an object (using a constructor of the class) is common, I've put this code into a global function called CreateJavaObject. This is the code for the CreateJavaObject utility function:
function CreateJavaObject(const JNIEnv: TJNIEnv; const ClassName: string; var AClass: JClass): JObject;
var
  Mid: JMethodID;
begin
  Result := nil;

    // Find the class
  try
    AClass := JNIEnv.FindClass(PChar(ClassName));
    if AClass = nil then
      Exit;

      // Get its default constructor
    Mid := JNIEnv.GetMethodID(AClass, '<init>', '()V');
    if Mid = nil then
      exit;

      // Create the object
    Result := JNIEnv.NewObjectA(AClass, Mid, nil);

  except
    on E: Exception do
      ShowMessage('Error: ' + E.Message);
  end;
end;
There are two inputs and two outputs for this function. The inputs are the Java environment, JNIEnv: TJNIEnv, and the class name, Classname: string. The function returns two pieces of information to the caller. The first value it returns (via the Result variable) is the newly created Java object. The second value is a JClass value, which is returned through the JClass var parameter. This value is required by the calling function.

Note that this isn't a general-purpose object creation function because the signature of the constructor ('()V') is hard-coded into the function. This function just gives you an idea of some of the utility functions that you may want to write to handle very common cases in your coding. This function is used by the OnClick handler for the TButton btnCallObjectMethod. This code shows the Java class being located, the object being constructed (via the CreateJavaObject function), the Fetch method being located and called.

procedure TURLGrabberForm.btnCallObjectMethodClick(Sender: TObject);
var
  Cls: JClass;
  Mid: JMethodID;
  PageGrabber: JObject;
  JStr: JString;
  URL, HTML: string;
begin
  UpdateStatusBar('Calling object method...', True);

  try
      // Get the URL from the UI
    URL := lstURLs.Text;

      // Construct PageGrabber object
    PageGrabber := CreateJavaObject(FJNIEnv, 'PageGrabber', Cls);
    if PageGrabber = nil then
    begin
      ShowMessage('Can''t create PageGrabber object');
      Exit;
    end;

      // Locate the 'Fetch' method
    Mid := FJNIEnv.GetMethodID(Cls, 'Fetch', '(Ljava/lang/String;)Ljava/lang/String;');
    if Mid = nil then
    begin
      ShowMessage('Can''t find method: Fetch');
      Exit;
    end;

      // Call the method
    JStr := FJNIEnv.CallObjectMethod(PageGrabber, Mid, [URL]);

      // Convert the returned JString to a Delphi string
    HTML := FJNIEnv.JStringToString(JStr);

      // Display the HTML
    mmoHTML.Lines.Add(HTML);

  except
    on E: Exception do
      ShowMessage('Error: ' + E.Message);
  end;

  UpdateStatusBar('Ready.');
end;

Calling a Method from Another Thread

Now, things get a little more interesting. As is the case with most threaded code, some special care needs to be taken to ensure proper functionality of the threads. The Java Native Interface is no exception. To solve the basic threading issues, only a few extra API calls need to be made.

There are some guidelines to follow if native (Delphi) code is to access the JNI from multiple threads. I am not going to go into a lot of detail about it because that is a larger topic that is beyond the scope of this tutorial. However, the place to go for detailed answers is Sheng Liang's The Java Native Interface - Programmer's Guide and Specification. I highly recommend this book to anyone interested in doing any serious JNI programming. A link to this book is in the resources section.

One of the points that is stressed when accessing the JNI from native threads is that you must not pass the JNIEnv pointer from one thread to another. The JNIEnv pointer is only valid in the thread that created it (or obtained it), so you can't save it or cache it for use in more than one thread. If you remember from the btnLoadVM event handler for the OnClick event, you will recall that we stored this pointer in a field of the TFrmMain class called FJNIEnv.

Now, you may be asking yourself, "If you can't store this value, why did we do that?". The reason is because we didn't use it in more than one thread. The UI for the application itself is running in a single thread, the main thread, and so any code that is executing in the main thread can use this stored value. The problem arises when you create other threads (worker threads) that need to access this JNIEnv pointer. And that is exactly what we are going to do now.

Important Linux Note:

In order to have Kylix interact with Java from threads other than the main UI thread, you must use version 1.3 (or later) of the Java Runtime Environment. You can download the Java 2 Runtime Environment version 1.3 for Linux from Sun's website here.

The first thing we need to do is to derive a new class that is based on the TThread class. This class is shown here:

  TURLThread = class(TThread)
  public
    URL: string;
    HTML: string;
    JavaVM: TJavaVM;
    constructor Create(const JavaVM: TJavaVM; const URL: string);
    procedure Execute; override;
    procedure UpdateUI;
  end;

As you can see, it is fairly simple. There are two points here that are relevant to the discussion of threads. The most important aspect of a TThread-derived class is its Execute method. This is where the thread does the actual work. If you look at this function, you will see that it is very similar to the other functions in the program, specifically, the OnClick event handler for btnCallObjectMethod. There are only two essential changes that are noted in the code with 3 asterisks (***) in the comment:
procedure TURLThread.Execute;
var
  Cls: JClass;
  Mid: JMethodID;
  PageGrabber: JObject;
  RetVal: JString;
  Env: PJNIEnv;
  JNIEnv: TJNIEnv;
begin

  try

      // *** Attach this thread to the running JVM
    JavaVM.JavaVM^.AttachCurrentThread(JavaVM.JavaVM, @env, nil);

      // Create the TJNIEnv wrapper class from the environment just retrieved
    JNIEnv := TJNIEnv.Create(env);

      // Create an instance of the PageGrabber Java object
    PageGrabber := CreateJavaObject(JNIEnv, 'PageGrabber', Cls);

      // Locate the method we wish to call
    Mid := JNIEnv.GetMethodID(Cls, 'Fetch', '(Ljava/lang/String;)Ljava/lang/String;');
    if Mid = nil then
    begin
      ShowMessage('Can''t find method: Fetch');
      Exit;
    end;

      // Call the method
    RetVal := JNIEnv.CallObjectMethod(PageGrabber, Mid, [URL]);

      // Convert the JString to a Delphi string
    HTML := JNIEnv.JStringToString(RetVal);

      // This will display the HTML in a "safe" manner
    Synchronize(UpdateUI);

      // *** Release the reference to this thread (important!)
    JavaVM.JavaVM^.DetachCurrentThread(JavaVM.JavaVM);

  except
    on e : Exception do
      ShowMessage('Error: ' + e.Message);
  end;
end;
These two additional lines of code are describe below. Each function is part of the JavaVM record in the Invocation API. The TJavaVM class contains a member (a property) of type PJavaVM (a pointer to a JavaVM.) Through this property, we can access the functions necessary to support accessing the Java Native Interface from Delphi threads. There are several other aspects of the TURLThread class, but none of them deal directly with the Java Native Interface, so I won't discuss them here. Refer to the Delphi online help for additional information on the TThread class. The code that is called to actually instantiate a TURLThread object is show in the btnCallMethodFromThreadClick event procedure below:
procedure TURLGrabberForm.btnCallMethodFromThreadClick(Sender: TObject);
var
  URLThread: TURLThread;
  URL: string;
begin
  mmoHTML.Clear;
  Application.ProcessMessages;
  URL := lstURLs.Text;
  URLThread := TURLThread.Create(FJavaVM, URL);
  URLThread.Resume;
end;

Creating Multiple Threads

The major task when developing a TThread-derived class is implementing its Execute method. Once that is complete, it is generally easy to create multiple instances of the class. This function, which is the OnClick event handler for the btnCreateMultipleThreads button shows just how easy it is to create several threads and have them all execute simultaneously. No changes to the TURLThread class are required.
procedure TURLGrabberForm.btnCreateMultipleThreadsClick(Sender: TObject);
var
  URLThread: TURLThread;
  I: Integer;
  URL: string;
begin
  UpdateStatusBar('Creating threads...', True);

    // Create a thread for each URL in the list
  for I := 0 to lstURLs.Items.Count - 1 do
  begin
    URL := lstURLs.Items[I];
    URLThread := TURLThread.Create(FJavaVM, URL);
    URLThread.Resume;
  end;

  UpdateStatusBar('Ready.');
end;

Back to top

Summary
Part Three of Using the Java Native Interface with Delphi focused on the Invocation API, which is part of the more general Java Native Interface API. The primary purpose of the Invocation API is to provide a mechanism for native applications (in our case Delphi applications) to access Java code. This is also known as "embedding" the Java Virtual Machine into a native application. Since the Java VM is implemented in a DLL, it is a trivial matter for Delphi to load this DLL at runtime and access its exported functions.

This tutorial is meant as an introduction to the Invocation API, not an exhaustive reference document. After going through this tutorial, you should have a pretty good understanding of what the Invocation API is and how to use it. For further information on the Invocation API or any other issues with the JNI, refer to one of the links listed in the resources section.

So.... what's next? Well, this was pretty much all that I had intended to show with my Using the Java Native Interface with Delphi. There are certainly more topics that could be discussed, but the main goal was to get you up to speed with this technology. I have mentioned Sheng Liang's book in the resources section of this tutorial. This is a must have for anyone wishing to further their understanding of the Java Native Interface. And, although it is targeted at C/C++ programmers, many (if not all) of the topics discussed in the book apply to Delphi as well. I just wish I would have discovered this book sooner.

Download the files used in this document.
Using the Java Native Interface with Delphi (Part One)
Using the Java Native Interface with Delphi (Part Two)
Helpful resources (from Part One.)

Back to top


Copyright © 2000-2002 Matthew Mead. All Rights Reserved.