Matthew Mead
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.
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.
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.
File | Purpose |
---|---|
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. |
There are basically four steps required to execute the example program. Let's look at each step
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
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.
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.
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.
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:
.\HelloWorld.java:3: Method void displayHelloWorld() requires a method body. Otherwise declare it as abstract. public void displayHelloWorld(); ^ Main.java:5: class HelloWorld is an abstract class. It can't be instantiated. HelloWorld hw = new HelloWorld(); ^ 2 errors
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.)
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:
The first parameter is a pointer to the runtime environment. The second parameter is a pointer to the object that called this method. The second parameter is sort of like the Self member of a class in Delphi. In this example, since the function is so trivial, we don't use either of these. However, it is important that you include them in the definition of the function because they will always be passed in to the function whether or not they are used. Later, we will see how and when to use these parameters.
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.
HelloWorld hw = new HelloWorld();
This line causes a new HelloWorld object to be created. When this class is created, the code inside it's static section executes:
System.loadLibrary("HelloWorldImpl");
The System.loadLibrary method causes the DLL (HelloWorldImpl.DLL) to be loaded by the operating system. If our DLL would have wanted to perform any kind of startup action or initialization, it would have performed it at this time. (For this simple example there is no initialization code.)
Also notice that the extension, .DLL, has been omitted. The reason for this is simple: Java is a cross-platform language and other operating systems may not name shared libraries with a DLL extension. For example, under Unix, the equivalent of HelloWorld.DLL would be libHelloWorld.so, which has an extension of .so instead of .DLL and also has the term lib prepended.
This is a call to the displayHelloWorld method in the HelloWorld object. However, since this method is a native method of the HelloWorld object, the call ends up going to the displayHelloWorld method (Java_HelloWorld_displayHelloWorld) implemented in HelloWorldImpl.DLL. This displays the text Hello World! on the screen. This "routing" of method calls from a Java object to a native-code DLL is all transparent to the calling class, Main.hw.displayHelloWorld();
Graphically, the process looks like this:
Figure 6: A graphical representation of the process at runtime. View a larger image here.
Let's trace the sequence of events depicted in Figure 6:
Whew. That sure seemed like a lot of work. Actually, the explanation of what is going on is far more verbose than the actual code. If you look at the Java files, you'll see that each file has about 4 lines of Java code. The Delphi DLL source file contains about 10 lines or so (depending how you want to format the code.) There's really only 1 line of executable code in the DLL; the line that prints 'Hello World!' to the screen. It's important to realize that (in the graphic above) steps 1 - 5 are executed only once, regardless of how many times hw.displayHelloWorld() (or any other native method of the HelloWorld class) is called. This is similar to how DLLs work with regular executable (.exe) programs: the DLL is loaded and any overhead associated with the loading and initialization of the DLL is done once at load time.
Also, this is just a very high-level view of what's going on. There are some pretty interesting things going on under the covers. But, that's beauty of it: you don't need to know or worry about those details.
For the most part, programming with the JNI is a pretty straight-forward process. However, there are a lot of details involved. As is true with most programming, one missing piece can cause the whole program to fail. In this section, we'll look at some of the common mistakes that you are likely to make. (I've made them all at one time or another!) Understanding the error messages and exceptions will help you to locate the problems within your code. I was able to cause all of these errors by modifying the example above. Hence, some of the error messages will refer to the files in the HelloWorld example. If you are trying this example on your own I highly encourage you to try and cause these errors to occur. You will see first-hand what I'm talking about and learn from these common errors.
All of these examples were developed with Sun's Java 2 SDK, version 1.2.2. If you are using a later version, perhaps version 1.3.x, you may or may not experience the same errors.
By far, the most popular is the java.lang.UnsatisfiedLinkError:
java.lang.UnsatisfiedLinkError
There are basically two categories of UnsatisfiedLinkError errors. This type of error prevents the Delphi code from executing because the Java runtime environment is unable to locate the native code.
This message says that Java can't locate the HelloWorldImpl DLL. Here are some common causes:Exception in thread "main" java.lang.UnsatisfiedLinkError: no HelloWorldImpl in java.library.path
static { System.loadLibrary("HelloWorldImpl"); // correct System.loadLibrary("HelloWorldImpl.dll"); // WRONG! }
This message indicates that the HelloWorldImpl DLL was located and loaded by the system. However, it says it can't find the indicated method (displayHelloWorld in the example.) There are several reasons that this might happen:Exception in thread "main" java.lang.UnsatisfiedLinkError: displayHelloWorld at Main.main(Main.java:6)
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.
A subtle difference in this exception as opposed to other exceptions is that the method call fails immediately. What I mean to say is that the call itself causes the exception. None of the code is executed within the native method. This can be helpful because some exceptions (as we'll see shortly) occur when the method returns, so all of the code within the method is executed. When I remove the stdcall directive in our example, the text Hello World! never gets displayed. This is an indication that the calling convention is incorrect.
This is the message I get when I remove the stdcall directive:
This is the error message I receive:procedure Java_HelloWorld_displayHelloWorld(PEnv: PJNIEnv; Obj: JObject; Extra: JInt); stdcall;
The subtle difference between this exception and the previous one (where I removed the stdcall directive) is that this time the text Hello World! is displayed on the screen before the error message is displayed. It's subtle differences like this that can help you locate where the problems are.
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:
This displays the Run Parameters dialog box:
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.
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.
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 Type | Delphi Type | JNI Alias in Delphi | Description |
---|---|---|---|
boolean | Byte | JBoolean | 8 bits, unsigned |
byte | Shortint | JByte | 8 bits, signed |
char | WideChar | JChar | 16 bits, unsigned |
double | Double | JDouble | 64 bits |
float | Single | JFloat | 32 bits |
int | Longint | JInt | 32 bits, signed |
long | Int64 | JLong | 64 bits, signed |
short | Smallint | JShort | 16 bits, signed |
void | N/A | void | N/A |
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 Type | Mapping |
---|---|
boolean | Z |
byte | B |
char | C |
double | D |
float | F |
int | I |
long | J |
void | V |
object | Lfully-qualified-class; |
type[] | [type |
method signature | (arg-types) ret-type |
Examples:
Java Method | Signature |
---|---|
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.
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:
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;
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.
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
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.
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.
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
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:
Let's look at each of these steps in detail:
// 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.
// 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.
// 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.
// 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.
// 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;
// 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.
// 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;
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:
// 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.
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.
JNI Specifications
Books:
Delphi on the Web:
My other JNI work: