Inhalt | 4. Sensoren

Mit Java Fortran-Code ausführen

Einführung: JNI

Um aus Java heraus nativen Code aufzurufen, wird von Sun die JNI bereitgestellt. JNI steht für Java Native Interface und beinhaltet Spracherweiterungen für Java um C-Code aufzurufen und C-Header um Java-Code von C aus aufzurufen. Für Fortran gibt es nichts dergleichen, deshalb muss man den Fortran-Code mit einem Gerüst aus C umgeben, den Fortran-Code mit dem C-Gerüst in eine Shared Library linken um dann von Java aus über C den Fortran-Code ausführen zu können. Wie das funktinioniert soll hier beschrieben werden.

Ein Beispiel in Fortran

Folgendes Beispiel (fdummy.f) wurde bereitgestellt und von mir um Kontrollausgaben erweitert:
C --------------------------------------------------------------------
C FORTRAN DUMMY ROUTINE
C --------------------------------------------------------------------
C
C VARIABLEN:
C
C   IPAR: PARAMETER (INTEGER)
C   RPAR: PARAMETER (DOUBLE PRECISION)
C   IVEC: ARRAY     (INTEGER, 1 x IPAR)
C   RVEC: ARRAY     (DOUBLE PRECISION, 1 x IPAR)
C   IMAT: ARRAY     (INTEGER, IPAR x IPAR)
C   RMAT: ARRAY     (DOUBLE PRECISION, IPAR x IPAR)
C
C --------------------------------------------------------------------


      SUBROUTINE FCN(IPAR,RPAR,IVEC,RVEC,IMAT,RMAT)
      IMPLICIT DOUBLE PRECISION (A-H,O-Z)

      DIMENSION IVEC(IPAR),RVEC(IPAR),IMAT(IPAR,IPAR),RMAT(IPAR,IPAR)

      WRITE(*,*) 'F77: DUMMY AUFGERUFEN'

      RPAR = 0.0D0

      WRITE(*,*) 'F77: Float Vektor'
      WRITE(*,*) (RVEC(i), i=1,10)

      WRITE(*,*) 'F77: Float Matrix'
      DO 50 i=1,10
        WRITE(*,*) (RMAT(i,j), j=1,10)
50    CONTINUE

      DO 100 I=1,IPAR
        IVEC(I) = 0
        RVEC(I) = 0.0D0
100   CONTINUE

      DO 150 I=1,IPAR
        DO 150 J=1,IPAR
          IMAT(I,J) = 0
          RMAT(I,J) = 0.0D0
150   CONTINUE

      WRITE(*,*) 'F77: DUMMY WIRD BEENDET'

      RETURN
      END
Dieses Beispiel können wir mit dem GNU Fortran77 Compiler in ein Object-File compilieren:
$ g77 -c -fPIC fdummy.f
Das erstellt uns ein fdummy.o Object File, womit wir Fortran fürs erste Beiseite legen können.

Das Java-Programm

Zunächst müssen wir uns die C-Funktionen und deren Parameter überlegen, die später die Fortran-Funktion aufrufen. Also müssen wir bereits an diese Funktionen sämtliche Parameter übergeben, die an Fortran weitergegeben werden sollen. Für unser Beispiel genügt eine Funktion cdummy, die wir als private native deklarieren und natürlich nicht implementieren. Außerdem initialisieren wir gleich die JNI indem wir ihr den Namen der Shared Library übergeben (ohne lib am Anfang und .so am Ende. Unsere Library heißt also libldummy.so):
class jdummy {
  private native void cdummy(int i, double d, int i_arr[]double d_arr[]int i_2arr[][]double d_2arr[][]);

[...]

  static {
    System.loadLibrary("ldummy");
  }
}
Als nächstes eine Funktion
public void run() {

[...]

}
in der wir Arrays erzeugen,
int i_arr[] new int[MAXSIZE];
double d_arr[] new double[MAXSIZE];
int i_2arr[][] new int[MAXSIZE][MAXSIZE];
double d_2arr[][] new double[MAXSIZE][MAXSIZE];
initialisieren
for (int i=0; i<MAXSIZE; i++) {
  i_arr[i= i;
  d_arr[i= i*1.0;
  System.out.println(i + "   " + i_arr[i"   " + d_arr[i]);
  for (int j=0; j<MAXSIZE; j++) {
    d_2arr[i][j= i*(j+1)*1.0;
    i_2arr[i][j= MAXSIZE*i + j;
  }
}
und schließlich cdummy(...) aufrufen:
cdummy(MAXSIZE, 5.0, i_arr, d_arr, i_2arr, d_2arr);
Im Großen und Ganzen reicht dies aus, um Skalare Werte, Vektoren und Matrizen an ein Fortran-Programm zu übergeben. Hier nochmal der gesamte Code im Überblick (jdummy.java):
class jdummy {

    final static int MAXSIZE = 10;

    private native void cdummy(int i, double d, int i_arr[]double d_arr[]int i_2arr[][]double d_2arr[][]);

    public static void main(String args[]) {
        jdummy jd = new jdummy();
        jd.run();
    }

    public void run() {
        System.out.println("J: -- Ausfuehren des Java-Code --");

        int i_arr[] new int[MAXSIZE];
        double d_arr[] new double[MAXSIZE];
        int i_2arr[][] new int[MAXSIZE][MAXSIZE];
        double d_2arr[][] new double[MAXSIZE][MAXSIZE];

        System.out.println("J: Initialisieren saemtlicher Vektoren und Matrizen");
        for (int i=0; i<MAXSIZE; i++) {
            i_arr[i= i;
            d_arr[i= i*1.0;
            System.out.println(i + "   " + i_arr[i"   " + d_arr[i]);
            for (int j=0; j<MAXSIZE; j++) {
                d_2arr[i][j= i*(j+1)*1.0;
                i_2arr[i][j= MAXSIZE*i + j;
            }
        }


        System.out.println("J: C-Code aufrufen");

        cdummy(MAXSIZE, 5.0, i_arr, d_arr, i_2arr, d_2arr);

        System.out.println("J: -- Zurueck in Java --");

        System.out.println("J: Inhalt der beiden Vektoren");
        for (int i=0; i<MAXSIZE; i++)
            System.out.println(i + "   " + i_arr[i"   " + d_arr[i]);

        System.out.println("J: Ausgabe der zurueckgegebenen int-Matrix");
        for(int i=0; i<MAXSIZE; i++) {
            for(int j=0; j<MAXSIZE; j++) {
                System.out.print(i_2arr[i][j"   ");
            }
            System.out.println("");
        }

        System.out.println("J: Exit Java");
    }

    static {
        System.loadLibrary("ldummy");
    }
}
Auch dieses Sourcefile compilieren wir, wie gewohnt mit
$ javac jdummy.java

Die Verbindung: C

Nun benötigen wir nur noch die Verbindung zwischen Java und Fortran: C. Hierfür lassen wir zuerst ein Headerfile erzeugen.
$ javah -jni jdummy
Erstellt eine Headerdatei "jdummy.h" im aktuellen Verzeichnis, die wir gleich in unser C-File einbinden:
#include <stdio.h>
#include <stdlib.h>

// Header von javah -jni erstellt
#include "jdummy.h"
Als nächstes fügen wir die Deklaration der Fortranfunktion ein. Die Funktion muss als extern deklariert werden und ans Ende des Funktionsnamens muss ein _ angehängt werden. Variablen müssen grundsätzlich als Referenz übergeben werden, Arrays als Zeiger.
//                               = int *, = double *
extern void fcn_(int *, double *, int [], double [], int *, double *);
Die Definition der Funktion cdummy in C kann man einfach aus der von Java generierten Headerdatei kopieren. Lediglich die Namen der Parameter sollte man einfügen, um innerhalb der Funktion darauf zugreifen zu können:
JNIEXPORT void JNICALL Java_jdummy_cdummy
   (JNIEnv *env, jobject jobj,                       // Standard Parameter, muessen bei jedem Aufruf uebergeben werden
    jint ji, jdouble jd,                             // Skalare Parameter
    jintArray ji_a, jdoubleArray jd_a,               // Vektoren
    jobjectArray ji_2a, jobjectArray jd_2a           // Die 2-dimensionalen Arrays (/ Matrizen)
   ) {
In dieser Funktion nun erstmal die Daten aus Java holen. Die Skalaren Werte können direkt verwendet werden, die Arrays und Objekte müssen erst kopiert werden:
    jsize n = ji;
    jint *i_a = (*env)->GetIntArrayElements(env, ji_a, 0);
    jdouble *d_a = (*env)->GetDoubleArrayElements(env, jd_a, 0);
Die zweidimensionalen Arrays sind etwas komplizierter, da sie von Java als ObjectArray von Arrays des Grunddatentyps gesehen werden:
    jsize i, j;

    // Die lokalen Arrays initialisieren
    int imat[n][n];
    double dmat[n][n];

    // Nun die 2-dimensionalen Arrays
    for(i=0; i<n; i++) {
        jintArray i_tmp = (jintArray)((*env)->GetObjectArrayElement(env, ji_2a, i));
        jint * ji_tmp = (*env)->GetIntArrayElements(env, i_tmp, 0);
        for(j=0; j<n; j++) {
            imat[i][j] = ji_tmp[j];
        }
    }

    for(i=0; i<n; i++) {
       	jdoubleArray d_tmp = (jdoubleArray)((*env)->GetObjectArrayElement(env, jd_2a, i));
        jdouble * jd_tmp = (*env)->GetDoubleArrayElements(env, d_tmp, 0);
        for(j=0; j<n; j++) {
            dmat[i][j] = jd_tmp[j];
        }
    }
Jetzt kann man die Fortran-Funktion aufrufen und die Skalaren Werte und Arrays übergeben:
    fcn_(&n, &jd, i_a, d_a, (int*)imat, (double*)dmat);
Zusammen mit einigen Ausgaben zur Verifikation der übergebenen Daten könnte ein Beispielprogramm so aussehen:
#include <stdio.h>
#include <stdlib.h>

// Header von javah -jni erstellt
#include "jdummy.h"

/*
 * FORTRAN Funktionen muessen als extern deklariert werden
 * Parameter werden als call-by-reference uebergeben
 * Damit die Funktion in der Library gefunden wird, muss ein "_" angehaengt werden
 */
//                               = int *, = double *
extern void fcn_(int *, double *, int [], double [], int *, double *);

/*
 * Um eine C-Funktion von Java aus aufzurufen, muss ihr Name wie folgt zusammengesetzt werden:
 *   "Java_" + Name der Klasse, die den C-Code aufruft + "_" + Name der Funktion
 * in diesem Fall:
 *   Java_jdummy_cdummy
 *
 * Die ersten beiden Parameter muessen immer uebergeben werden, der Rest variiert nach Bedarf,
 * was man eben vom Java-Programm aus uebergeben will.
 * Diese Deklaration kann man meist aus dem von javah erzeugten Headerfile kopieren
 */

JNIEXPORT void JNICALL Java_jdummy_cdummy
   (JNIEnv *env, jobject jobj,                       // Standard Parameter, muessen bei jedem Aufruf uebergeben werden
    jint ji, jdouble jd,                             // Skalare Parameter
    jintArray ji_a, jdoubleArray jd_a,               // Vektoren
    jobjectArray ji_2a, jobjectArray jd_2a           // Die 2-dimensionalen Arrays (/ Matrizen)
   ) {

    printf("C: -- Ab jetzt: C --\n");

    //  Die Daten aus Java holen

    // Erst die Eindimensionalen Arrays
    jsize n = ji;
    jint *i_a = (*env)->GetIntArrayElements(env, ji_a, 0);
    jdouble *d_a = (*env)->GetDoubleArrayElements(env, jd_a, 0);

    jsize i, j;

    // Die lokalen Arrays initialisieren
    int imat[n][n];
    double dmat[n][n];

    // Nun die 2-dimensionalen Arrays
    for(i=0; i<n; i++) {
        jintArray i_tmp = (jintArray)((*env)->GetObjectArrayElement(env, ji_2a, i));
        jint * ji_tmp = (*env)->GetIntArrayElements(env, i_tmp, 0);
        for(j=0; j<n; j++) {
            imat[i][j] = ji_tmp[j];
        }
    }

    for(i=0; i<n; i++) {
       	jdoubleArray d_tmp = (jdoubleArray)((*env)->GetObjectArrayElement(env, jd_2a, i));
        jdouble * jd_tmp = (*env)->GetDoubleArrayElements(env, d_tmp, 0);
        for(j=0; j<n; j++) {
            dmat[i][j] = jd_tmp[j];
        }
    }

    // Zur Probe: Ausgabe einiger Daten
    printf("C: Inhalt des Integer Arrays, von Java uebergeben:\n");
    for (i=0; i<n; i++)
        printf("%2d %5d\n", i, i_a[i]);

    printf("C: Inhalt der 2-dim double-Matrix\n");
    for (i=0; i<n; i++) {
        for (j=0; j<n; j++) {
            printf("%5f   ", dmat[i][j]);
        }
        printf("\n");
    }

    printf("C: Inhalt der 2-dim int-Matrix\n");
    for (i=0; i<n; i++) {
        for (j=0; j<n; j++) {
            printf("%5d   ", imat[i][j]);
        }
        printf("\n");
    }

    printf("-- Aufruf des FORTRAN-Codes --\n");
    fcn_(&n, &jd, i_a, d_a, (int*)imat, (double*)dmat);
    printf("-- Zurueck im C-Programm --\n");

    printf("C: Inhalt des Integer-Arrays\n");
    for (i=0; i<n; i++)
       printf("%2d %5d\n", i, i_a[i]);

    printf("C: Inhalt der 2-dim double-Matrix\n");
    for (i=0; i<n; i++) {
        for (j=0; j<n; j++) {
            printf("%5f   ", dmat[i][j]);
        }
	    printf("\n");
    }
    printf("C: Inhalt der 2-dim int-Matrix\n");
    for (i=0; i<n; i++) {
        for (j=0; j<n; j++) {
            printf("%5d   ", imat[i][j]);
        }
	    printf("\n");
    }

    return;
}
Dieses speichern wir als cdummy.c und compilieren es mit dem GCC:
$ gcc -c -D_REENTRANT -fPIC -I/usr/lib64/jvm/java-1.5.0-sun-1.5.0_10/include -I/usr/lib64/jvm/java-1.5.0-sun-1.5.0_10/include/linux
Wobei man u.U. die Pfadangaben bei den -I Optionen anpassen muss. Obige Zeile ist korrekt für den WAP-Pool. Abschließend noch die beiden Object-Files zu einer Library zusammenfügen
$ gcc -shared cdummy.o fdummy.o -lg2c -o libldummy.so
und den Suchpfad auf die frisch erstellte Library umlenken:
$ export LD_LIBRARY_PATH=`pwd`
Mit
$ java jdummy
startet man nun das Java-Programm.


1: http://java.sun.com/j2se/1.4.2/docs/guide/jni/spec/jniTOC.html
2: http://en.wikipedia.org/wiki/Java_Native_Interface
3: http://www.csharp.com/javacfort.html

Nach oben | Inhalt | 4. Sensoren