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.INC and get started. The source files used in this part of the 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 is 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.

The tutorials have been updated to reflect Kylix, the Linux version of Delphi. These documents were originally written before Kylix, so naturally they were Windows-only. However, most (if not all) of the material applies to Kylix and Linux as well. Rather than re-write a separate document for Kylix (which would be about 98% identical in content) or use the phrases "Delphi or Kylix" and "DLL or SO" everywhere, I've left it Windows-specific. I've tried to point out any differences between the two environments, but may have missed something along the way. When I say Delphi, I am referring to both Delphi and Kylix. When I say DLL, I am referring to both DLLs (.DLL) on Windows and shared libraries (.so) on Linux.

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); {$IFDEF WIN32} stdcall; {$ENDIF} {$IFDEF LINUX} cdecl; {$ENDIF}
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:
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.)

There is now a tool available that will produce a Delphi/Kylix project file (.dpr) from a Java class file (.class). It is available from my Delphi/JNI home page. There are actually two tools: a Java-based one (JavaToDPR) and a native Delphi-based one (javadpr) that do the same thing.

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

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); {$IFDEF WIN32} stdcall; {$ENDIF} {$IFDEF LINUX} cdecl; {$ENDIF}
7.  begin
8.    WriteLn('Hello world!');
9.  end;
10.
11. exports
12.   Java_HelloWorld_displayHelloWorld;
13. 
14. end.
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.)

      Technically, the class name is the fully qualified name, including the package name. These examples are trivial, so packages are not used. If your Java class resides in a package, make sure to include the package name as part of the class name.

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

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 © 2000-2002 Matthew Mead. All Rights Reserved.


web analytics