[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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).
A.1 Introduction A.2 Implementing a Native Method in Ada A.3 Interfacing to an Existing Ada API A.4 Calling a Java Method from Ada A.5 Using Ada Objects from Java A.6 Using Java Objects from Ada
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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:
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).
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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:
These steps will now be described in more detail.
A.2.1 A Java class with a native method A.2.2 Generating an Ada specification A.2.3 Implementing the native method A.2.4 Compiling to a shared library or DLL A.2.5 Running the program
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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 library, `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'
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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:
_
(underscore) characters
precede the ParamsSignature component of the name.
_
component is absent if the Java class is defined
in the default (anonymous) package.
(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.
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
Writing the JNI-compliant Ada specification manually is tedious;
GNAT-AJIS includes the javastub
tool to automate this step
by generating an appropriate Ada spec from a Java class file
containing a native method to be implemented in Ada:
$ javastub Example1.class |
This command, the Ada analog to javah -jni
for C, will generate the
package spec shown above.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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 |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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_Method_ID
, declared as follows:
function Get_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_Method_ID (Env, Class, "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_Method_ID (Env, Class, "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; |
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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
.
[ < ] | [ > ] | [ << ] | [ Up ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |
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_Method_ID (Env, Class, "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, "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 |
[ << ] | [ >> ] | [Top] | [Contents] | [Index] | [ ? ] |