Using the Java Native Interface with Delphi (Part One - Introduction and Tutorial)

Using the Java Native Interface with Delphi
(Part One - Introduction and Tutorial)

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

Quick StartIf you're familiar with Delphi and JNI and want to get started right away.
BackgroundWhy JNI? Why Delphi?
OverviewWhat's here?
TranslationA brief look at translating the C/C++ header file to Delphi.
ExampleThe canonical HelloWorld program.
A Closer LookThe HelloWorld program in excruciating detail.
TroubleshootingTypical errors when programming with the JNI.
DebuggingStepping through your Delphi DLL at runtime.
SummaryWhat's next?
Helpful ResourcesSources to help you with Delphi and the JNI
Quick Start
C/C++ programmers should check out these pages:

Programming with the Java Native Interface This is a tutorial that shows how to use the Java Native Interface with Linux, Solaris, and Windows NT.
Using the Java Native Interface with C++ This has a lot of examples of how you might use the Java Native Interface. Contrasts Sun's vision with Microsoft's. Also includes some crude benchmarks. This is an older document. (January 1998)
Experienced JNI/Delphi programmers should grab jni.pas and jni_md.pas and get started. The files are syntax-highlighted using a tool called YAPP, which is was written by John Kaster of Borland/Inprise.) Also, all of the source files used in this tutorial can be downloaded here.

Delphi programmers may also want to check out the C/C++ links above. Although those pages are targeted at C/C++ programmers, there's a wealth of information about the Java Native Interface (not necessarily related to C/C++) that may not be duplicated here.

Back to top

Background
A couple of years ago I did some exploration of the Java Native Interface. At that time, everything "native" was being done in C/C++. That really wasn't a problem, though, since the syntax of the Java language targets C++ programmers. (This is clearly evident by the similarities between the two languages.) Now, this isn't to say that Java is identical to C++ (or unlike other languages for that matter.) It's just an observation of the syntax of the two languages. Also, all of the available JNI documentation/references were aimed at C/C++ programmers.

My first go-round with the JNI is documented here. I basically just presented my findings (albeit, crude.) There wasn't much documentation around at the time (circa January 1998), so I didn't have too many references. After that initial exposure, I refined the experiment. That result was more of a tutorial and can be found here.

Since the time of those experiments, I have continued to receive occasional emails from people asking for further assistance. When I was able to help, I did. Until recently, I hadn't even looked at the JNI stuff. That changed a few weeks ago when I decided to try implementing the native portion using Borland/Inprise's Delphi language. The result: The acronyms 'JNI' and 'RAD' can now be used in the same sentence. I'm not going to get into a debate over why one would want to use native code from within Java code, I'm just going to show you how to do it. Again, this document is aimed specifically at Delphi programmers. C/C++ programmers would want to check out one or both of the links mentioned above.

Back to top

Overview
Before starting this, you should be able to compile and execute Java code on your machine. This document focuses on getting you up to speed with Delphi and the Java Native Interface. I'm not an expert on Java, and setting up and configuring a compile-time and run-time environment sometimes seems harder than actually writing the Java code (although I think that the Java 2 stuff makes it a little easier.)

Another important point about this Delphi interface is that it hasn't been tested thoroughly. I present several different "tests" (examples), but there are over 200 distinct functions in the API of the JNI. I'm probably only using a couple dozen at most. But, you'll see how similar they are. In fact, 90% of the Delphi interface looks pretty repetitive. Once you get the "pattern" down, you can easily translate any function's signature. For Part One, though, you shouldn't have any problems because I'm really not using any of the JNI API.

One last thing: I used version 5.0 of Delphi to develop this. I'm not sure how the other versions will handle it. If I find the time, I will run the tests through the other versions. (I have all 5 versions of Delphi, I just don't want to spend the time installing/uninstalling them!!) If anyone tries this with another version, please feel free to let me know how it went.

Back to top

Translation Notes

First off, it's important to note that this isn't the only possible translation. As I will show later, you can translate a type incorrectly (loosely speaking), but as long as the two types have the same size, everything will work. As a preview to this, you'll see a lot of things typed as Pointer, which is very generic, and as far as the JNI is concerned, any type that is exactly 4-bytes wide (at the time of this writing) is compatible with a Delphi pointer.

Originally, I had planned to explain the jni.pas in detail. But, I've decided to leave that for a more advanced topic. It's not that the translation is advanced (quite the contrary), it's just that it isn't necessary to understand the details to start using the JNI with Delphi, which I'm assuming is why you're here. I will describe a couple of key points about the translation from C/C++ to Delphi. If you are not interested (and it's quite alright if you are not interested) you can skip to the Example below. If you do wish to know more, you should look at these:

The two most important guidelines when translating a C/C++ header file (interface) is to make sure that:

Size

It is important that the data types that are passed between Java and native code are the same size. The table below shows this mapping. The types labeled as Native Type are the types that will be defined in Delphi. This is safer than trying to remember that a long in Java is an Int64 in Delphi, or that an int in Java is a Longint in Delphi. Also, there is no void type in Delphi, but there is a 'void pointer', Pointer, which we will see a lot. This table is not exhaustive. It just shows the primitive types.

Java TypeDelphi TypeC++ TypeNative TypeDescription
booleanBooleanunsigned charjboolean8 bits, unsigned
byteShortintcharjbyte8 bits, signed
charWideCharunsigned shortjchar16 bits, unsigned
doubleDoubledoublejdouble64 bits
floatSinglefloatjfloat32 bits
intLongintlongjint32 bits, signed
longInt64__int64jlong64 bits, signed
shortSmallintshortjshort16 bits, signed
voidN/AvoidvoidN/A

The type definitions below are excerpts from jni_md.pas and jni.pas which show how Delphi types are mapped into Java types:

type

    (*
     * These are Win32-specific types
    *)
  JInt  = Integer;
  JLong = Int64;
  JByte = Shortint;

    (*
     * JNI Types
    *)
  JBoolean = Byte;
  JChar    = WideChar;
  JShort   = Smallint;
  JFloat   = Single;
  JDouble  = Double;

Preview:

Here are a couple of Java functions:
  double CalculateAverage(int A, int B, int C);
  void printString(String value);
and this is how they would appear in Delphi:
  function CalculateAverage(A: JInt; B: JInt; C: JInt): JDouble;
  procedure printString(Value: JString);
Actually, the functions would look more like this:
  function Java_Native_CalculateAverage(PEnv: PJNIEnv; Obj: JObject; A: JInt; B: JInt; C: JInt); stdcall;
  procedure Java_Native_printString(PEnv: PJNIEnv; Obj: JObject; Value: JString); stdcall;
but we're not there yet. At this point, it's important that you recognize the correlation between Java types and Delphi types.
Order

The majority of the methods declared in jni.pas are methods of a this record:

  JNINativeInterface_ = packed record
    reserved0   : Pointer;
    reserved1   : Pointer;
    reserved2   : Pointer;
    reserved3   : Pointer;
    GetVersion  : function(Env: PJNIEnv): JInt; stdcall;
	  
     (* . . . Over 200 additional methods declared here . . . *)

  end;
Every translation of the C/C++ header file jni.h must declare the methods of a record in the exact same order.

Note that the order of the records in the Delphi file is not important. Only the order of the methods within the records is important.

Back to top

A HelloWorld Example

One of the best ways to learn a new programming skill is by example. In keeping with tradition, I present the canonical programming example that simply prints the words "Hello World!" to the display. The twist is that Java code will invoke a native function to do the actual printing via the Delphi library function, WriteLn.

1. Create the Java files. First, create the two Java files as shown below.

HelloWorld.java:
class HelloWorld
{
  public native void displayHelloWorld();
  static
  {
    System.loadLibrary("HelloWorldImpl");
  }
}
Main.java:
class Main 
{
  public static void main(String[] args)
  {
    HelloWorld hw = new HelloWorld();
    hw.displayHelloWorld();
  }
}
2. Compile the Java files.
javac HelloWorld.java
javac Main.java
3. Create and build the Delphi file. You will end up with a file called HelloWorldImpl.dll.
HelloWorldImpl.dpr:
library HelloWorldImpl;

uses
  JNI;

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

exports
  Java_HelloWorld_displayHelloWorld;

end.
4. Execute the program. You should open a command window (DOS prompt) in the directory where your Delphi and Java files are and execute it there.
java Main

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

Back to top

A Closer Look at the HelloWorld Example

1. Create the Java Files
HelloWorld.java Create the Java class that declares the native method. In this example, the Java class definition is trivial. There are no members or methods defined within the class. There are 2 key points here:
  • Make sure to include the native keyword when declaring the native method, otherwise, it's just another Java class method. This also tells the JVM at runtime that the function is implemented in a DLL somewhere and not in this class.
  • Also, be sure that the name of the shared library is correct. Note that the name of the DLL does not have to have the same name as the class that exposes the method. In the example, the name of the class is HelloWorld and the name of the DLL is HelloWorldImpl. (The static section of a class is executed when the class is loaded. We use this section to load the DLL that contains the implementation of the native method.)
class HelloWorld
{
  public native void displayHelloWorld();
  static
  {
    System.loadLibrary("HelloWorldImpl");
  }
}
Additional note: There is a tool that comes with the JDK called javah. This is a 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. (Note to self: Write a javah-like tool that produces a Delphi-compatible file.)

Main.java Create the Java class that will test the native method.

  • Notice that this Java code has 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() ever changes.
class Main
{
  public static void main(String[] args)
  {
    HelloWorld hw = new HelloWorld();
    hw.displayHelloWorld();
  }
}
2. Compile the Java files.
javac HelloWorld.java
javac Main.java
This, of course, compiles the Java files and creates two files, HelloWorld.class and Main.class, respectively.
3. Create and build the Delphi file.
Now things are getting a little interesting. There are several points here that you should take notice of. I'll go through this code in much more detail than the Java code above. That's the main purpose of this document: I want to show you how to use Delphi to implement native methods to be called from Java. I've also included line numbers below, so that I can easily refer to the source code.

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.)

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

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.
  • Line 1: Since this is a DLL, and not an executable program (.exe), we use the library keyword to tell Delphi to create a DLL file. The name of the DLL will be HelloWorldImpl.dll.

  • Line 4: This is probably the most important line (if there can be only one.) This file, jni.pas, contains the interface to the JNI. This will be used by every DLL that needs to be accessed by Java. You can basically consider it a black box. However, you have the source, so you can view it to get a better understanding of what's going on behind-the-scenes.

  • Line 6: There are several pieces of important information here that must be specified with each function that is going to be called from Java:

    1. This function is actually a Delphi procedure. This is simply because there is no return value. Had the Java class declared the method to return a value, you would have seen the function keyword instead. We will see functions later on. Unless otherwise specified, when I refer to a Delphi function I am also talking about a Delphi procedure as well.

    2. All functions/procedures that you want to be accessed from a Java class must have the term Java_ prepended to them. Also, the name of the class that declares the native method (HelloWorld in our example) must follow the Java_ term. An underscore character follows the Java class name and, finally, the name of the method itself. In our example, the fully decorated function name is Java_HelloWorld_displayHelloWorld. Unlike Delphi, the Java language is case-sensitive, so you must make sure that the Delphi function definitions use the same case as the Java method declarations. (Another reason for a tool like javah. Maybe call it javad? Any takers?)

    3. What gives with the parameters? There were no parameters specified in the Java class! Every Java method that is implemented in native code will have two additional parameters. They are both pointers that the native function can use to access functions in the Java object that called the function and to access functions in the Java runtime environment. The types PJNIEnv and JObject are both defined in jni.pas

      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 heavy use of these parameters.

    4. Unless you have written DLLs before or have had to access code written in another language (or implemented a Windows callback), you may have never used the stdcall directive. This tells Delphi to generate code that will push the arguments onto the stack from right to left. Delphi uses the fast register calling convention by default, which uses a combination of registers and the stack to pass arguments to functions. It's not important to understand the details, but if you forget to add the stdcall directive, your DLL will most surely cause an exception.

  • Line 8: This is what we are actually trying to accomplish! It seems like a lot of work to print a line of text, and it is. However, it's doubtful that anyone would go through this much trouble to print a line of text when they could clearly do it from Java. But, as you probably have already figured out, in the real world we would be using/implementing some serious native code that just isn't possible or efficient to implement in Java.

  • Line 12: In order for our function to be accessible from the outside, we must export it. This is done in the exports section of a Delphi library (DLL.) Notice that you don't provide the parameters or function/procedure keyword.
4. Execute the program. In a nutshell, this is what's happening:
java Main    <--- you type this at the console
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 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 executables (.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.

Back to top

Troubleshooting
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, I will show you most of the 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 cause these errors to occur. You will see first-hand what I'm talking about and learn from these common errors. By far, the most popular is the java.lang.UnsatisfiedLinkError:

java.lang.UnsatisfiedLinkError

There are basically two categories of UnsatisfiedLinkError errors.

  1. The first type of java.lang.UnsatisfiedLinkError looks something like:
    Exception in thread "main" java.lang.UnsatisfiedLinkError: no HelloWorldImpl
    in java.library.path
    
    This message says that Java can't locate the HelloWorldImpl DLL. Here are some common causes:

    • The DLL is named incorrectly. Make sure that the name of the DLL you are loading in the static section matches the name of the DLL on the disk. Be sure that you are not including the .dll extension in the call to System.loadLibrary:
        static
        {
          System.loadLibrary("HelloWorldImpl");         // correct
          System.loadLibrary("HelloWorldImpl.dll");     // WRONG!
        }
      
    • The DLL is not in the search path. In Windows (DOS) by default, the directory you are in (at a command prompt) is included in the search path. If you want Java to be able to find the DLL from another directory, make sure that you add that directory to the system path or move the DLL into a directory that is already in the system path.

  2. Another java.lang.UnsatisfiedLinkError error might look like this:
    Exception in thread "main" java.lang.UnsatisfiedLinkError: displayHelloWorld 
    at Main.main(Main.java:6)
    
    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:

    • The name of the method is incorrect. Make sure that the name in the DLL matches the name in the Java class exactly. You may have made one or more of these errors:

      • You spelled the method name differently. (It happens!)
      • The case is incorrect. Remember, Java is case-sensitive and Delphi is not, so you may have inadvertently used the wrong case.
      • You decorated the method name incorrectly. It must start with Java_ then the class name HelloWorld, then and underscore _, then the name of the method displayHelloWorld. (For classes other than our HelloWorld example, you would substitute that class name in place of HelloWorld.)

    • The method is not exported. The method must be added to the exports section of the project file. (It is common to forget this when you first start writing DLLs.)

    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.

    • Missing stdcall directive. The most common mistake made by Delphi programmers is forgetting to add the stdcall directive at the end of the procedure/function. (I'm guessing about the other Delphi programmers because, frankly, I haven't talked with any other Delphites about their JNI experiences. I'm just assuming that they would make this mistake because I made it often!)

      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:

    • Incorrect procedure/function signature. This happens when the number, type, and order of parameters in the Java class don't match the number, type, and order of the parameters in the Delphi code. In our example, the displayHelloWorld method was declared as taking 0 parameters. In the Delphi code, we made sure that the procedure expected 0 parameters. (This is not counting the two hidden arguments I discussed that all JNI methods include.) If we add a parameter to the Delphi procedure (an extra integer parameter) we can force this error to happen:
      procedure Java_HelloWorld_displayHelloWorld(PEnv: PJNIEnv; Obj: JObject; Extra: JInt); stdcall;
      
      This is the error message I receive:

      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.

    Back to top

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

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.

Note that you will not be able to step through the Java code. The Delphi debugger has no knowledge of the Java code. Maybe the next release of JBuilder from Inprise/Borland will support this.

Back to top

Summary
In this document, I showed you how easy it is to access code from a DLL that was created with Delphi. However, 95% of what I discussed applies equally well to C/C++ or any other native language. This introduction to Using the Java Native Interface with Delphi is more of a tutorial than a reference. If you remove all of the detailed explanations above, you will see that the amount of code necessary to call a DLL from Java is quite trivial. Newcomers to Delphi, DLLs, and/or JNI may find this detail necessary and enlightening. More advanced developers may already understand these points.

Before proceeding onto Part Two, I strongly encourage you to try this example on your own. The main reason is to prove to yourself that your machine is properly configured to compile and execute Java code. If you are currently able to do so, then it is likely that this example will work just fine. It's a lot easier to troubleshoot problems when the example is trivial.

Download the files used in this tutorial.
Using the Java Native Interface with Delphi (Part Two)
Using the Java Native Interface with Delphi (Part Three)

Back to top

Helpful Resources
JNI Specifications Books: Delphi on the Web: My other JNI work:

Back to top


Copyright © 2001 Matthew Mead All Rights Reserved.