This material is published with permission of Informant Communications Group, Inc. and Delphi Informant magazine.

Greater Delphi

Java Native Interface / Java / JBuilder / Delphi 6, 7

By Keith Wood

Going Native: Part 2

Calling Java Code from a Delphi App

In Part 1, you saw how Delphi code could be called from a Java application using the Java Native Interface (JNI). By following certain naming conventions, and by using specific data types and structures within your native code, your Java classes can easily invoke these routines, passing parameters and results between the two. Exceptions can also be generated and handled on either side of the interface. This installment will demonstrate how Java code can be called from a Delphi application, fulfilling the bi-directional promise of JNI.

The ease-of-use of Delphi and Java interactions is due to the efforts of Matthew Mead, who has produced a Pascal translation of the JNI suitable for use in Delphi. If you haven’t already, you can download the package from Matthew’s Web site (see "References" at the end of this article).

Loading the Java VM

To illustrate how to invoke a Java class from Delphi, you can use the JavaD utility described in Part 1 and provide a Delphi GUI for it (see Figure 1). As you’ll recall, this tool generates a Pascal skeleton based on a given Java class’ native methods to simplify the use of JNI with Delphi. Written in Java, the utility makes use of Java’s reflection abilities to discover the native methods and their signatures.

The generateDelphiWrapper method in this class takes three parameters: the full name of the Java class to examine, the output directory for the Pascal file, and a flag indicating whether any existing file can be overwritten.

Figure 1: Delphi GUI for the JavaD tool.

Before you can call any Java code from Delphi, you must first load the Java virtual machine (JVM). Add the JNI unit to your application, then start by preparing the parameters for the JVM. You have the option of using a VM that conforms to the JNI 1.1 or 1.2 specifications. The differences are mainly concerned with versioning of JNI, reflection support, and enhancements of existing capabilities. This choice affects the format of parameters passed to the initial call: JDK1_1InitArgs or JavaVMInitArgs. In this case, use JNI 1.2 and the latter type. The arguments are the normal parameters you would pass to the JVM if you were starting it from the command line. Only the classpath option is handled here, after having been entered by the user:

-Djava.class.path=<user-defined class path>

Next, create a TJavaVM object, then use it to load the actual JVM with the arguments set above, as shown in Figure 2. Check for and report on any error condition during the load. If no problems arise, initialize a TJNIEnv object based on the loaded JVM. You are then ready to continue and invoke the Java code itself. Note that it’s impossible to unload the JVM, so be sure your parameters are correct before loading it.

{ Load the JVM }
procedure TfrmJavaD.btnLoadClick(Sender: TObject);
var
  ClassPath: string;
  Errcode: Integer;
  VM_args: JavaVMInitArgs;
  Options: array [0..10] of JavaVMOption;
begin
  ShowStatus('Loading Java VM...');
  // Confirm the classpath.
  ClassPath := InputBox('JavaD','Enter the Java classpath',
    'C:\Projects\DJNI\classes');
  // Set up the options for the VM
  FillChar(Options, SizeOf(Options), #0);
  Options[0].optionString :=
    PChar('-Djava.class.path=' + ClassPath);
  VM_args.version  := JNI_VERSION_1_2;
  VM_args.options  := @Options;
  VM_args.nOptions := 1;
  // Create the wrapper for the VM
  FJavaVM := TJavaVM.Create;
  // Load the VM
  Errcode := FJavaVM.LoadVM(VM_args);
  if Errcode < 0 then begin
    // Loading the VM more than once will cause this error.
    if Errcode = JNI_EEXIST then
      raise Exception.Create('Java VM has already ' +
        'been loaded. Only one JVM can be loaded.')
    else
      raise Exception.Create(Format(
        'Error creating JavaVM, code = %d', [Errcode]));
  end;
  // Create the Env class
  FJNIEnv := TJNIEnv.Create(FJavaVM.Env);
  ShowStatus(Format('Java VM %d.%d loaded',
    [FJNIEnv.MajorVersion, FJNIEnv.MinorVersion]));
  // Adjust buttons
  btnLoad.Enabled     := False;
  btnLoad.Default     := False;
  btnGenerate.Default := True;
  edtChange(nil);
end;

Figure 2: Loading the Java VM.

Calling Java Code

To call a method on a Java class or object, you must first obtain a reference to that class or object. You locate the class through the FindClass function of the Delphi TJNIEnv class. If successful, it returns a reference to the class. Otherwise, it returns nil.

Having obtained the class reference, you must then locate the required method through its name and signature using GetMethodID or GetStaticMethodID from TJNIEnv. Check that the returned value is valid (not nil) and report an error if not. Then call the appropriate invocation routine, depending on the method’s return type and whether the method is static. In this case, because the return value is a String (an object), and the method is static, you use CallStaticObjectMethod. Convert the result into a Delphi string through the JStringToString method.

Because this process involves several steps and the actual Delphi function called depends on both the return type and its static status, the JNIUtils unit includes two calls that make it much simpler. If the return type is a primitive type, a String, or void, then invoke CallMethod. Use the CallObjectMethod function if the return type is of any other object (including an array). The former routine returns a Variant value, encapsulating all the basic types (including void as Null), and the latter returns a generic JObject reference.

Both routines take a reference to the JNI environment, the class (for static methods) or object reference, the name of the method, its signature (as Java types), an array of parameter values that match the signature, and an optional Boolean to indicate a static (True) or non-static method (False, the default) method. You can see both versions in the demonstration code (available for download; see end of article for details). Which one is used depends on the presence or absence of the JNIUTILS conditional definition.

Following a method call, you should check whether any exceptions arose in the Java code. Start by calling the ExceptionOccurred method of the JNI environment, which returns nil if no exception was thrown, or a reference to the exception that cropped up. If not nil, your first step should be to clear the JNI flag indicating that the exception occurred using ExceptionClear (otherwise some further calls will also fail). You can then proceed through the normal course of obtaining further details about the exception by calling its methods. In this case, find out its class name and message, and display these to the user. As before, both direct and JNIUtils versions of the code are included.

If the Pascal skeleton generation succeeded, the generateDelphiWrapper method returns the name of the created file. You can use this to load its contents into a memo and display them to the user. The code for all of this is shown in Listing One.

Run the application to test it. Load the JVM by clicking the left button. Confirm or update the classpath to use. Remember that the path must include the directories at the top of the class file hierarchies of both the JavaD class and the class that you wish to generate from. Separate multiple path definitions with semi-colons ( ; ). After the JVM is loaded, the status bar shows the version of JNI in use.

Enter the name of the class to examine, your output directory, and whether you will allow existing files to be overwritten. Click Generate to start the process. If successful, the output is loaded into the memo for your perusal. If a Java exception occurs, it’s reported to you as a class name and message. To observe some of the possible Java errors that may arise, try entering an invalid class name or clearing the overwrite flag.

Conclusion

The Java Native Interface lets you call Java code from native code, or vice versa. Through the efforts of Matthew Mead, a Delphi version of the JNI is available, making it very simple to use.

To call Java code you must first load the Java virtual machine, setting appropriate parameters. Then locate the classes required, find the methods on them to invoke, and call these with the appropriate parameters. The demonstration program described here provides a Delphi GUI to the JavaD tool, allowing you to easily generate Delphi skeleton files for JNI from your Java classes.

Using the facilities described in this article, you can now mix and match between Delphi and Java as the need arises. See the documentation that comes with Matthew’s Delphi/JNI package for more details and examples.

JavaD and the Java and Delphi demo programs referenced in this article are available for download.

References

JNI Specification: http://java.sun.com/j2se/1.4.1/docs/guide/jni/index.html

JNI FAQ: http://java.sun.com/products/jdk/faq/jnifaq.html

Delphi/JNI Home: http://home.pacifier.com/~mmead/jni/delphi/index.html

Keith Wood hails from Australia, where he is a consultant working with Java and Delphi, and a freelance technical writer. He started using Borland’s products with Turbo Pascal on a CP/M machine. His book, Delphi Developer’s Guide to XML, 2nd Edition from BookSurge, covers many aspects of XML from a Delphi point of view. You can reach him via e-mail at kbwood@iprimus.com.au.

Begin Listing One — Calling the Java class

{ Generate a Pascal skeleton for JNI. }
procedure TfrmJavaD.btnGenerateClick(Sender: TObject);
var
  Cls: JClass;
{ $IFNDEF JNIUTILS }
  MID: JMethodID;
{ $ENDIF }
  Exc: JThrowable;
  FileName: string;
  ErrMsg: string;
begin
  try
    memOutput.Lines.Clear;
    ShowStatus('Calling generator method...');
    // Find JavaD class
    Cls := FJNIEnv.FindClass(
      'wood/keith/opentools/javad/JavaD');
    if Cls = nil then
      raise Exception.Create('Can't find class: ' +
        'wood.keith.opentools.javad.JavaD');
    // Run it
{ $IFDEF JNIUTILS }
    FileName := JNIUtils.CallMethod(FJNIEnv, Cls,
      'generateDelphiWrapper',
      'String (String, String, boolean)',
      [edtClassName.Text, edtDirectory.Text,
      chkOverwrite.Checked], True);
{ $ELSE }
    MID := FJNIEnv.GetStaticMethodID(Cls,
      'generateDelphiWrapper',
      '(Ljava/lang/String;Ljava/lang/String;Z)' +
      'Ljava/lang/String;');
    if MID = nil then
      raise Exception.Create('Can't find method: ' +
        'generateDelphiWrapper');

    FileName := FJNIEnv.JStringToString(
      FJNIEnv.CallStaticObjectMethod(Cls, MID,
      [edtClassName.Text, edtDirectory.Text,
      chkOverwrite.Checked]));
{ $ENDIF }
    // Check for exception.
    Exc := FJNIEnv.ExceptionOccurred;
    if Exc <> nil then
    begin
      // Clear the exception so we
      // can call other methods.
      FJNIEnv.ExceptionClear;
      // Find out about the exception -
      // its class and message.
{ $IFDEF JNIUTILS }
      Cls := JNIUtils.CallObjectMethod(
        FJNIEnv, Exc, 'getClass', 'Class()', []);
      ErrMsg := JNIUtils.CallMethod(
        FJNIEnv, Cls, 'getName', 'String()', []) +
        #13 + JNIUtils.CallMethod(
        FJNIEnv, Exc, 'getMessage', 'String()', []);
{ $ELSE }
      MID := FJNIEnv.GetMethodID(
        FJNIEnv.GetObjectClass(Exc),
        'getClass', '()Ljava/lang/Class;');
      if MID = nil then
        raise Exception.Create(
          'Can't find method: getClass');
      Cls := FJNIEnv.CallObjectMethod(Exc, MID, []);
      MID := FJNIEnv.GetMethodID(
        FJNIEnv.GetObjectClass(Cls),
        'getName', '()Ljava/lang/String;');
      if MID = nil then
        raise Exception.Create(
          'Can't find method: getName');
      ErrMsg := FJNIEnv.JStringToString(
        FJNIEnv.CallObjectMethod(Cls, MID, []));
      MID := FJNIEnv.GetMethodID(
        FJNIEnv.GetObjectClass(Exc),
          'getMessage', '()Ljava/lang/String;');
      if MID = nil then
        raise Exception.Create(
          'Can't find method: getMessage');
      ErrMsg := ErrMsg + #13 +
        FJNIEnv.JStringToString(
        FJNIEnv.CallObjectMethod(Exc, MID, []));
{ $ENDIF }
      raise Exception.Create(
        'A Java exception occurred'#13 + ErrMsg);
    end;
    // Load the generated file.
    memOutput.Lines.LoadFromFile(FileName);
    ShowStatus('Done');
  except
    on E: Exception do begin
      ShowStatus('Error');
      MessageDlg('Error: ' + E.Message,
        mtError, [mbOK], 0);
    end;
  end;
end;

End Listing One