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()
andFoo()
, 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
andFoo_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.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