.. _Using_JNI_Directly: ****************** 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). .. _Using_JNI_Directly_Introduction: 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). .. _Implementing_a_Native_Method_in_Ada: 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. .. _A_Java_class_with_a_native_method: 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_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 ``.class`` .. _Generating_an_Ada_specification: 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___`` ``Java_____`` Please note the following: * Two consecutive ``_`` (underscore) characters precede the ```` component of the name. * The ``_`` component is absent if the Java class is defined in the default (anonymous) package. * The ```` 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`` .. _Implementing_the_native_method: 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. .. _Compiling_to_a_shared_library_or_DLL: 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 .. _Using_JNI_Directly_Running_the_program: 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. .. _Interfacing_to_an_Existing_Ada_API: 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. .. _Calling_a_Java_Method_from_Ada: 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; .. _Using_Ada_Objects_from_Java: 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``. 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