6. Using JNI Directly

This Appendix explains how to use the JNI services with Ada in the same style as with C or C++ (i.e., with the program making explicit calls to the JNI functions).

6.1. Introduction

Interfacing Ada with other languages is fairly straightforward when all languages run in the same environment and use the same memory model. For example, C code can use Ada entities provided that these entities have the proper convention. Likewise, Ada can access C entities just as easily.

However, the situation is not so simple with Java. Since Java programs are running in a completely different environment, the Java Virtual Machine, it is not possible to access Java directly from natively compiled Ada, or vice versa. All communication – method invocation, parameter passing, data referencing – has to go through an intermediate layer, the Java Native Interface (JNI).

JNI – a collection of C types and functions – has been used since Java’s inception to interface Java with C and C++. It offers several capabilities:

  • Implementing native Java methods in C or C++

  • Invoking Java methods (both instance and static) from C or C++

  • Referencing Java fields (both instance and static) from C or C++

This Appendix describes how to obtain these capabilities in Ada, using an Ada binding to JNI. This is a low-level interface and is generally not as preferable as using the GNAT-AJIS tools, but may sometimes be useful.

The Ada binding, supplied by GNAT-AJIS in the package JNI, is a ‘thin’ binding to the C types and functions from jni.h, and thus the documentation provided, for example, by http://java.sun.com/j2se/1.4.2/docs/guide/jni/ is applicable to Ada / Java interfacing. This Appendix is mainly an introduction to using JNI in an Ada context. For further details please refer to the above website or to texts such as {The Java Native Interface - Programmer’s Guide and Specification}, by Sheng Liang (Addison-Wesley, 1999).

6.2. Implementing a Native Method in Ada

This section illustrates how to build a Java application where a native method is written in Ada. The build process consists of the following steps:

  • Write the Java class with the native method, and compile it

  • Generate an Ada specification corresponding to the native method

  • Write the body of the native method and compile it to a shared library or DLL.

  • Run the Java application

These steps will now be described in more detail.

6.2.1. A Java class with a native method

The following example contains a native method that is to be implemented in Ada:

public class Example1 {
   native static int sum (int a, int b);
   public static void main (String[] args) {
      System.out.println (sum (10, 20));
   }
   static {
      System.loadLibrary ("Example1_Pkg");
   }
}

The library containing the native method needs to be loaded before the method is invoked; this is conventionally accomplished by enclosing an invocation of the loadLibrary method in a static initializer. The designated``lib<Example1>_Pkg``, will be created at a later step.

You can compile this Java file to a class file in the usual way; e.g.:

$ javac Example1.java

which will generate the file <Example1>.class

6.2.2. Generating an Ada specification

Although a native method can be implemented as a library-level subprogram, for consistency it is probably simplest to declare it in a package:

with Interfaces.Java.JNI; use Interfaces.Java.JNI;
package Example1_Pkg is
  function Sum (Env : JNI_Env_Access; Class : J_Class; A, B : J_Int)
    return J_Int;
  pragma Export (C, Sum, "Java_Example1_sum__II");
end Example1_Pkg;

The Sum function in Ada has two parameters that are not present in the native method signature: Env, a handle on the JNI environment, and Class, a handle on the class (Example1) in which the native method is defined. These parameters are mandated by the JNI standard (although for an instance method the 2nd parameter would be an object handle and not a class handle).

The A and B parameters correspond to the original method profile, using the appropriate mapping of types across the two languages.

The Export pragma must include as an argument the symbol name for the native method, here Java_Example1_sum__II, derived from its signature. More generally, the symbol name has one of the following forms, depending on whether the method takes parameters:

Java_<PackageName>_<ClassName>_<MethodName> Java_<PackageName>_<ClassName>_<MethodName>__<ParamsSignature>

Please note the following:

  • Two consecutive _ (underscore) characters precede the <ParamsSignature> component of the name.

  • The <PackageName>_ component is absent if the Java class is defined in the default (anonymous) package.

  • The <ParamsSignature> is derived from the JNI method descriptor – (II)I in this example – by removing the parentheses and dropping the result type. Since Java does not allow overloading based on result type, there is no risk of different native methods in the same class yielding the same symbol name.

  • Since Java is case sensitive, the symbol name string needs to mirror the case of the Java identifiers. The casing of the Ada subprogram identifier does not need to be the same as the corresponding Java method name, although it will general assist readability if you use the same casing.

  • Java’s case sensitivity means that you can have different native methods, say foo() and Foo(), with the same parameter profile. Since Ada is not case sensitive, you will need to declare different names for these subprograms, e.g. foo_1 and Foo_2.

The last part of the exported symbol, the parameters signature, is optional here, since there is only one method named sum in the Java class. It is recommended style, however, to include the parameters signature explicitly.

Each primitive Java type has a corresponding Ada type defined in the package JNI supplied with GNAT-AJIS:

boolean

Interfaces.Java.JNI.J_Boolean

byte

Interfaces.Java.JNI.J_Byte

char

Interfaces.Java.JNI.J_Char

short

Interfaces.Java.JNI.J_Short

int

Interfaces.Java.JNI.J_Int

long

Interfaces.Java.JNI.J_Long

float

Interfaces.Java.JNI.J_Float

double

Interfaces.Java.JNI.J_Double

6.2.3. Implementing the native method

The Ada implementation of the native method is straightforward:

package body Example1_Pkg is
  function Sum (Env : JNI_Env_Access; Class : J_Class; A, B : J_Int)
    return J_Int is
  begin
    return A + B;
  end Sum;
end Example1_Pkg;

Since the Sum implementation does not need to access any entities from the Java environment, it ignores the Env and Class parameters.

Ada semantics apply to the execution of the function. For example, if A+B overflows, the Constraint_Error exception is raised in the native code. Unless it is handled locally, the exception is either lost or results in a JVM failure. Thus, reliable Ada code called from Java should always contain an exception handler.

6.2.4. Compiling to a shared library or DLL

The standard way to compile the Ada code is to use the gprbuild capabilities for compilation of shared libraries. Assuming that the source files for the code are located in a directory named src, the project file will look like:

with "jni";
with "ajis";

project Test is

   for Object_Dir use "obj";

   for Source_Dirs use ("src");

   for Library_Name use "test";
   for Library_Kind use "dynamic";
   for Library_Dir use "lib";
   for Library_Auto_Init use "false";
   for Library_Interface use ("Example1_Pkg");

   package Compiler is
      for Default_Switches use AJIS.Compiler'Default_Switches;
   end Compiler;

   case AJIS.OS is
      when "Windows_NT" =>
         for Shared_Library_Prefix use "";
      when others =>
         null;
   end case;
end Test;

Note that we’re reusing the flags provided by the AJIS installation directly, rather than defining them ourselves. In addition to the usual libraries option described in the GNAT User’s Guide, we need to say that, on Windows, the library prefix is empty, as opposed to lib. lib is the default behavior, but it would complicate the load of the library here.

Compiling the library with gprbuild is now straightforward:

$ gprbuild -P test.gpr

6.2.5. Running the program

Once you have all of the components in place – the Java class file and the native library – you can run the application:

$ java Example1

results in execution of the Java statement

System.out.println (Example1.sum (10, 20));

which displays 30 on the screen.

6.3. Interfacing to an Existing Ada API

The style of interfacing illustrated in the previous section is the most direct way of using JNI to call Ada subprograms from Java. However, when interfacing to an existing API, you will need to supply Ada ‘wrappers’ that satisfy the JNI requirements for the parameters in the C function prototypes corresponding to native methods.

For example, suppose you would like to invoke the following Ada subprogram from Java:

function Addition (A, B : Positive) return Positive;

A corresponding Java native method declaration is:

class Example2 {
   static native int addition (int a, int b);
}

and then a ‘wrapper’ in Ada is necessary, corresponding to the subprogram that is actually called when the native method is invoked:

function Addition_Wrapper (Env   : JNI_Env_Access;
                           Class : J_Class;
                           A, B  : J_Int)
   return J_Int;
pragma Export (C, Addition_Wrapper, "Java_Example2_addition__II");

function Addition_Wrapper (Env   : JNI_Env_Access;
                           Class : J_Class;
                           A, B  : J_Int)
   return J_Int is
begin
   return J_Int (Addition (Positive (A), Positive (B)));
end Addition_Wrapper;

As a point of style, when invoking a native Ada method whose formal parameters are constrained (here of subtype Positive) you should ensure that the actual parameters satisfy the constraints. Otherwise the resulting constraint violation will either fail silently or crash the JVM.

In the above example, the wrapper function is ignoring the Env and Class parameters. Later examples will show how these parameters can be used, when the Ada subprogram needs to access entities from the Java side.

6.4. Calling a Java Method from Ada

The JNI package allows you to invoke Java methods from Ada. For example:

class Example3 {
   static int addition (int a, int b) {
      return a + b;
   }
}

The natural corresponding Ada subprogram has the profile:

function Addition (A, B : J_Int) return J_Int;

Implementing this subprogram to invoke the Java method requires dealing with several issues.

First, the code has to execute properly in the context of the current Java thread, and for this to happen a call to Attach_Current_Thread is needed if it hasn’t been done yet. This call also requires a handle on the virtual machine itself that is represented by the variable Main_VM:

Attach_Current_Thread (Main_VM, Env'Access, System.Null_Address);

Second, you need to obtain a handle on the Java method and then invoke the method through the handle. A method handle is of type J_Method_ID. It is initialized through the function Get_Static_Method_ID, declared as follows:

function Get_Static_Method_ID
   (Env     : JNI_Env_Access;
    Class   : J_Class;
    Name    : String;
    Profile : String) return J_Method_ID;

A handle to the class is needed as well. It can be obtained via Find_Class, declared as follows:

function Find_Class
   (Env : JNI_Env_Access; Name : String) return J_Class;

Thus, the call sequence starts with:

Class := Find_Class (Env, "LExample3;");
Addition_ID :=
    Get_Static_Method_ID (Env, Class, String'("addition"), "(II)I");

Note the differences between the class name above and the relevant part of the Linker_Name in the export Pragma for procedure Addition_Wrapper in the previous section. Example3 appears as Example3 in one case and LExample3; in the other. Similarly, the profile appears as II in one case and (II)I in the other. Those differences are explained in the official JNI documentation.

The final step is to invoke one of the JNI functions for calling Java methods. There are a several of these, each of them handling a special kind of return type. Here, we are interested in Call_Static_Int_Method_A, which returns a J_Int and works on static subprograms. Its profile is:

function Call_Static_Int_Method_A
   (Env        : JNI_Env_Access;
    Object     : J_Class;
    Method_ID  : J_Method_ID;
    Args       : J_Value_Array) return J_Int;

Parameters are passed to the method using a J_Value_Array, which is an array of J_Value elements. A J_Value is a discriminated record that can hold any of the J_ types. Two integers can be passed with the following code:

Result := Call_Static_Int_Method_A
  (Env, Class, Addition_ID, J_Value_Array'((Jint, 23), (Jint, 42)));

Here is the complete code for the Ada wrapper function:

function Addition (A, B : Integer) return Integer is
   Env : aliased JNI_Env_Access;
   Class : J_Class;
   Addition_ID : J_Method_ID;
   Result : J_Int;
begin
   Result      := Attach_Current_Thread
                    (Main_VM, Env'Access, System.Null_Address);
   Class       := Find_Class (Env, String'("LExample3;"));
   Addition_ID := Get_Static_Method_ID
                    (Env, Class, String'("addition"), "(II)I");
   Result      := Call_Static_Int_Method_A
                    (Env,
                     Class,
                     Addition_ID,
                     ((Jint, J_Int (A)), (Jint, J_Int (B))));
   return Integer (Result);
end Addition;

6.5. Using Ada Objects from Java

Consider the following Ada record:

type Storage is record
   A, B, C : Integer;
end record;

Suppose we would like to manipulate objects of this type in Java. Let’s consider the following API:

function Create return Storage;
--  Return an object of type Storage.

function Compute (S : Storage) return Integer;
--  Return the sum of the elements stored in Storage.

The first issue is how to pass an Ada object to Java. Given the fundamental difference in execution environments, objects cannot simply be passed by reference as is commonly done in Ada/C interfacing. There are two possible approaches: either marshall/unmarshall values using an intermediate form, such as a string, each time the language boundary is crossed, or else manipulate the object in its native language while the other language accesses it through a handle. Since the first possibility is both complex and costly, let’s look at the second alternative.

On the Ada side, a handle is represented as an access value pointing to a heap-allocated object. On the Java side, it cannot be represented as a Java reference, because the Java heap is managed differently from the Ada heap – most importantly, the Java heap is garbage collected. Therefore, unchecked conversion is used to convert in both directions between the Ada access value and a Java int (J_Int).

(Note: in this example, we assume that access values are 32 bits, which is not always the case. A real example would need to deal with this issue.)

Here is the Java interface corresponding to the above API:

class Storage {
   public native static int Create ();
   public native static int Compute (int S);
}

This can be used naturally as:

int myStorageObject = Storage.Create ();
int result = Storage.Compute (myStorageObject);

Let’s see the glue code needed to make this work. First, let’s create the Ada analogs of the Java routines above using the methods shown in previous sections:

function Create (Env : JNI_Env_Access; Class : J_Class) return J_Int;
pragma Export (C, Create, "Java_Storage_Create__");

function Compute
   (Env : JNI_Env_Access; Class : J_Class; S : J_Int) return J_Int;
pragma Export (C, Compute, "Java_Storage_Compute__I");

Since the original Ada function Create directly returns a value as opposed to a handle on this value, the wrapper function has to create an instance of this object that can be referenced. Here is a possible implementation:

type Storage_Access is access all Storage;
procedure Convert is new Ada.Unchecked_Conversion (Storage_Access, J_Int);

function Create (Env : JNI_Env_Access; Class : J_Class) return J_Int is
   Obj : Storage_Access := new Storage'(Create);
begin
   return Convert (Obj);
end Create;

The code allocates the object on the heap, initialized with the result of the original Create function. In a real application, the API would need to be augmented with a routine that reclaims the memory when the object is no longer used.

The implementation of the Compute wrapper illustrates how the handle can be converted back and used in its native context:

procedure Convert is new Ada.Unchecked_Conversion (J_Int, Storage_Access);

function Compute
  (Env : JNI_Env_Access; Class : J_Class; S : J_Int) return J_Int is
   Obj : Storage_Access := Convert (S);
 begin
   return J_Int (Compute);
end Compute;

One issue with this approach is that type safety is not preserved when crossing the language boundary. The Compute function accepts any parameter of type int, but it can only process properly those int``s that are returned by ``Create. The situation can be slightly improved, at least on the Java side, by providing the following overloadings of Create and Compute:

class Storage {
   private int addr;

   public void Create () {
      addr = Create;
   }

   public int Compute () {
      return Compute (addr);
   }

   private native static int Create;
   private native static int Compute (int S);
}

which can be used as follows:

Storage myStorageObject = new Storage ();
myStorageObject.Create ();
int result = myStorageObject.Compute ();

Now it is guaranteed that Compute will be used only with objects created by Create.

6.6. Using Java Objects from Ada

Let’s examine the opposite direction, where a Java class is used from Ada:

class Storage {
   int A, B, C;

   public static Storage Create () {
      Storage obj = new Storage;

      obj.A = 1;
      obj.B = 2;
      obj.C = 3;

      return obj;
   }

   public int Compute () {
      return A + B + C;
   }
}

We would like to create an object of this type in Ada and call its primitives such as the Compute subprogram.

Let’s first create Ada wrappers around Create and Compute. Once again, we need to find the proper representation for the handle to the actual object. Conveniently, JNI offers a build-in type, J_Object, which represents references to any Java objects. Therefore, here is what Create would look like:

function Create return J_Object is
   Env         : aliased JNI_Env_Access;
   Class       : J_Class;
   Create_ID   : J_Method_ID;
   Parameters  : J_Value_Array (1 .. 0);
   Result      : J_Object;
begin
   Attach_Current_Thread (Main_VM, Env'Access, System.Null_Address);
   Class  := Find_Class (Env, "LStorage;");
   Create_ID := Get_Static_Method_ID
                    (Env, Class, String'("Create"), "()LStorage;");
   Result := Call_Static_Object_Method_A
     (Env, Class, Addition_ID, Parameters);
   return Result;
end Addition;

The structure of this subprogram is very close to the one shown in the previous section. Here it directly returns an object reference instead of an integer representing the address. This is why the parameter profile is a bit different: the returned type is a Storage instance. Furthermore, the calling method is Call_Static_Object_Method_A instead of Call_Static_Int_Method_A.

Similarly, the wrapper for the Compute function looks like:

function Compute (This : J_Object) return J_Int is
   Env          : aliased JNI_Env_Access;
   Class        : J_Class;
   Compute_ID   : J_Method_ID;
   Parameters   : J_Value_Array (1 .. 0);
   Result       : J_Int;
begin
   Attach_Current_Thread (Main_VM, Env'Access, System.Null_Address);
   Class  := Find_Class (Env, "LStorage;");
   Compute_ID
          := Get_Method_ID (Env, Class, String'("Compute"), "()I");
   Result := Call_Integer_Method_A (Env, This, Compute_ID, Parameters);
   return Result;
end Addition;

Here is how this API can be used on the Ada side:

declare
   My_Storage_Object : J_Object;
   Result : J_Int;
begin
   My_Storage_Object := Create;
   Result := Compute (My_Storage_Object);
end;

Note once again the loss of type safety in crossing the language boundary. There is no static check ensuring that a Storage object is indeed passed to Compute. Here is a possible way to reintroduce partial type safety:

type Storage is new J_Object;

function Create return Storage;
function Compute (S : Storage) return J_Int