19.04. Zugriff von C nach Java

Mit JNI können Sie nicht nur nativen Code in Java ausführen, es ist auch der umgekehrte Weg möglich: Sie können in einer nativen Bibliothek Methoden von Klassen und Objekten aufrufen und Attribute auslesen oder setzen.

Unser Ziel ist eine kleine Klasse (CToJavaTest), die folgende Methoden bereitstellt:

  • public List<String> generateList() – Erzeugt eine beliebige Liste
  • public static void printList(List<String> toPrint) – Gibt eine List<String> aus
  • public native void callJNI() – Aufruf unserer JNI Bibliothek

Außerdem beinhaltet Sie noch folgende Felder:

  • public static String staticStringValue – Ein statisches String-Attribut
  • private int intValue – Eine gewöhnliche Objektvariable

Die zugehörige native Bibliothek wird Methoden aufrufen, Rückgabewerte verarbeiten und Attribute manipulieren.

Über Sinn und Unsinn dieser Klasse lässt sich natürlich streiten. Sie ist jedoch ideal um Ihnen die Lerninhalte dieses Kapitels zu vermitteln. Werfen wir einen Blick auf diese Klasse:

package de.jbb.jni;

import java.util.*;

public class CToJavaTest {

  static {
    System.loadLibrary("ctojava"); 
  }

  public static String staticStringValue = "I'm a String!";

  private int intValue = 5;

  public List<String> generateList() {

    List<String> ret = new ArrayList<String>(intValue);
    for (int i = 0; i < intValue; i++) {
      ret.add("Java: Eintrag: " + (i + 1));
    }
    return ret;
  }

  public static void printList(List<String> toPrint) {

    System.out.println("Java: printList");
    for (int i = 0; i < toPrint.size(); i++) {
      System.out.println("\tJava: " + toPrint.get(i));
    }
  }

  public native void callJNI();

  public static void main(String[] args) {

    CToJavaTest ctj = new CToJavaTest();
    ctj.callJNI();
    System.out.println("Java: " + CToJavaTest.staticStringValue);
  }
}

Wir legen gleichzeitig noch eine Main-Methode an, in der wir uns ein neues Objekt dieser Klasse erzeugen, die native Methode aufrufen, und letztendlich das statische String-Attribut staticStringValue ausgeben. Sie ahnen es bereits: Wir werden diese Variable in unserer nativen Bibliothek manipulieren.

Der Java Code sollte für Sie keine Probleme darstellen. Widmen wir uns deshalb unserer C-Bibliothek zu. Nachfolgenden finden Sie den kompletten Quellcode, welchen ich Ihnen anschließend erläutern werde.

#include <jni.h>
#include <stdio.h>
#include "de_jbb_jni_CToJavaTest.h"

JNIEXPORT void JNICALL Java_de_jbb_jni_CToJavaTest_callJNI(JNIEnv *env, jobject obj) {

// CToJavaTest-Klasse auslesen
  jclass cls = (*env)->GetObjectClass(env, obj);

// jstring mit Text initialisieren
  const char* text = "Ich wurde von C gesetzt!";
  jstring jstr = (*env)->NewStringUTF(env, text);
// Adresse eines statichen Attributs auslesen
  jfieldID staticStringField = (*env)->GetStaticFieldID(env, cls, "staticStringValue", "Ljava/lang/String;");
// statisches Attribut setzen
  (*env)->SetStaticObjectField(env, cls, staticStringField, jstr);

// Adresse des Objekt-Attributs auslesen
  jfieldID intField = (*env)->GetFieldID(env, cls, "intValue", "I");
// Wert des Attributs auslesen (funktioniert auch mit private Attributen)
  int intValue = (*env)->GetIntField(env, obj, intField);

// Adresse einer Objekt-Methode auslesen
  jmethodID generateListMethod = (*env)->GetMethodID(env, cls, "generateList", "()Ljava/util/List;");
// Objekt-Methode aufrufen und Rückgabewert in eine Variable speichern
  jobject list = (*env)->CallObjectMethod(env, obj, generateListMethod);

// statische Methode auslesen
  jmethodID printList = (*env)->GetStaticMethodID(env, cls, "printList", "(Ljava/util/List;)V");
// statische Methode mit Parametern aufrufen
  (*env)->CallStaticVoidMethod(env, cls, printList, list);

// Größe der Liste auslesen
  jclass listClass = (*env)->GetObjectClass(env, list);
  jmethodID sizeMethod = (*env)->GetMethodID(env, listClass, "size", "()I");
  int listSize = (*env)->CallIntMethod(env, list, sizeMethod);

// Get-Methode zur Abfrage der Liste ermitteln
  jmethodID getMethod = (*env)->GetMethodID(env, listClass, "get", "(I)Ljava/lang/Object;");

// Ausgabe
  printf("C: intValue: %d\n", intValue);
  printf("C: printList reverse\n");
  for (listSize = listSize - 1; listSize > -1; listSize--) {
// jobject in jstring casten
    jstring cur = (jstring)(*env)->CallObjectMethod(env, list, getMethod, listSize);
// jstring nach C konvertieren
    const char *c_cur = (*env)->GetStringUTFChars(env, cur, 0);
    printf("\tC: %s\n", c_cur);
  }
  return;
}

Die ersten Zeilen unterscheiden sich nicht von bereits Bekanntem. Wie Sie sicherlich noch aus dem Einstiegs Kapitel zu JNI wissen, wird jeder Methode die aufrufende Klasse (bei statischen Methoden) bzw. das aufrufende Objekt (bei Objektmethoden) übergeben. Das ist in diesem Fall die Variable obj vom Typ jobject und repräsentiert hier ein Objekt der Klasse CToJavaTest. Zusätzlich benötigen wir in dieser Methode auch noch die Klasse des Objekts. Diese erhalten wir mit mit dem Aufruf GetObjectClass der JNI Umgebung:

jclass cls = (*env)->GetObjectClass(env, obj);

Auch hier wird, wieder zuerst die JNI Umgebung übergeben, gefolgt von dem jobject, dessen Klasse ermittelt werden soll. Das Resultat wird als jclass in eine Variable geschrieben.

Jetzt können wir uns ein wenig „austoben“. Zuerst verändern wir das statische String Attribut der Klasse CToJavaTest. Hierzu benötigen Sie einen jstring. Dieser kann über die Methode NewStringUTF mit einer C-Zeichenkette erzeugt werden. Zusätzlich muss auch hier mal wieder die JNI-Umgebung (im restlichen Kapitel wird auf die explizite Erwähnung dieses Parameters bei Funktionsaufrufen verzichtet) übergeben werden.

const char* text = "Ich wurde von C gesetzt!";
jstring jstr = (*env)->NewStringUTF(env, text);

Um das Attribut zu setzen, wird (ähnlich wie bei Reflection) zuerst eine Referenz auf das Attribut benötigt. Dies funktioniert bei statischen Attributen mit dem Aufruf GetStaticFieldID. Möchten Sie auf ein nicht statisches Attribut zugreifen, muss stattdessen die Funktion GetFieldID verwendet werden. Als Parameter wird die Klasse gesetzt, in der das Attribut zu finden ist, der Name des Attributs, und von welchem Typ (int, String, List, …) das Attribut ist. Dieser Typ wird in Form einer eindeutigen Signatur übergeben.

jfieldID staticStringField = (*env)->GetStaticFieldID(env, cls, "staticStringValue", "Ljava/lang/String;");

Handelt es sich beim Typen des Attributs um einen primitiven Datentyp, wird eine der folgenden Abkürzungen verwendet:

Java Abkürzung/Signatur
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void V

Ist das Attribut kein primitiver Datentyp, wird ein „L“, gefolgt vom vollständigen Klassennamen (inkl. Package) und einem abschließendem Semikolon gesetzt. Anstelle des Punktes, um Packages und Klassen voneinander zu trennen, wird hier ein normaler Schrägstrich verwendet. Bei Arrays wird vor die Signatur noch eine geöffnete, eckige Klammer geschrieben. Wenn Sie nun also einen int ansprechen möchten, schreiben Sie als letzten Parameter ein „I“, bei einem long-Array wäre es ein „[J“ und bei einem Object ein „Ljava/lang/Object;“ (das Semikolon am Ende ist wichtig!).

Den Datentyp „void“ gibt es natürlich nicht als Attribut. Sehr wohl aber als Rückgabewert einer Methode. Aber hierzu später mehr.

Beim Setzen des statischen Attributs, wird die Methode SetStaticObjectField mit der Klasse, in der das Attribut gesetzt werden soll, der Attribut-ID, und dem neuen Wert aufgerufen. Möchten Sie einen primitiven Datentyp setzen, muss im Funktionsnamen das Object durch den entsprechenden Typen ersetzt werden. Um einen int zu setzen, rufen Sie bspw. die Methode SetStaticIntField auf. Oder für einen boolean die Methode SetStaticBooleanField. Objektattribute werden ohne Static (bspw. SetObjectField oder SetIntField) im Methodennamen gesetzt. Außerdem wird das Objekt, in welchem das Attribut gesetzt werden soll, anstelle der Klasse übergeben.

(*env)->SetStaticObjectField(env, cls, staticStringField, jstr);

Als nächstes soll ein Objektattribut vom Typ int ausgelesen werden. Auch hier benötigen Sie zuerst wieder die ID. Wie das funktioniert, wissen Sie bereits.

jfieldID intField = (*env)->GetFieldID(env, cls, "intValue", "I");

Anschließend können Sie über GetIntField den Wert direkt als C-int ermitteln. Auch hier ist wieder das Int im Methodennamen durch bspw. Object oder Boolean austauschbar. Bei statischen Attributen muss wieder ein Static in den Aufruf eingeschoben werden (bsp.: GetStaticObject). Die Parameter sind identisch mit der zugehörenden Set-Methode. Lediglich der neu zu setzende Wert fällt selbstverständlich als letzter Parameter weg.

Werfen Sie noch einmal einen Blick in den Java Quellcode. Es fällt auf, dass das Attribut intValue private ist. Sie können also auch private Attribute abfragen.

int intValue = (*env)->GetIntField(env, obj, intField);

Beim Aufruf einer Methode ist das Vorgehen recht ähnlich. Zuerst muss die Methoden-ID über GetMethodID bzw. GetStaticMethodID ermittelt werden. Die Parameter sind identisch mit denen von GetFieldID. Lediglich die Signatur wird anders aufgebaut. Sie hat die Form „(Übergabeparameter ohne Leerzeichen aneinander gereiht)Rückgabewert. Ein paar Beispiele:

public void doSomething() – Signatur: „()V“
public void doSomething(int i) – Signatur: „(I)V“
public void doSomething(int i, boolean b) – Signatur: „(IZ)V“
public int getSomething(int i, boolean b, String str, double d) – Signatur: „(IZLjava/lang/String;D)I“

Damit Sie die ID der Methode generateList erhalten, ist folgender Code notwendig:

jmethodID generateListMethod = (*env)->GetMethodID(env, cls, "generateList", "()Ljava/util/List;");

Um die Methode aufzurufen, wird die Funktion CallObjectMethod verwendet (bzw. wieder mit einem Static für statische Methoden (CallStaticObjectMethod) oder einem primitiven Rückgabetyp (CallIntMethod)). Je nachdem, ob es sich um eine statische Methode oder nicht handelt, wird die Klasse oder das Objekt der Methode übergeben. Anschließend folgt die Methoden-ID. Falls die Methode noch Parameter erwarten sollte, werden diese hinten als optionale Parameter angehängt. Da dies für die generateList Methode nicht notwendig ist, lautet der Aufruf wie folgt:

jobject list = (*env)->CallObjectMethod(env, obj, generateListMethod);

In diesem jobject befindet sich nun die Liste, die die Methode generateList zurücklieferte.

Zum besseren Verständnis rufen wir jetzt noch die statische Methode printList mit einem Übergabeparameter auf.

jmethodID printList = (*env)->GetStaticMethodID(env, cls, "printList", "(Ljava/util/List;)V");
(*env)->CallStaticVoidMethod(env, cls, printList, list);

Die restlichen Codezeilen in der Klasse sind nun nur noch Wiederholungen und sollten ohne Probleme verstanden werden. Hierbei ist es das Ziel, die ermittelten Daten (intValue und der Inhalt unserer Liste) in C auszugeben. Damit dieses Ziel erreicht werden kann, müssen jedoch einige Vorbereitungen getroffen werden.

Um über die Liste zu iterieren, wird die Größe der Liste benötigt. Diese kann über den Methodenaufruf size() der Liste ermittelt werden. Sie holen sich also zuerst wieder die Klasse der Liste, anschließend die Methoden-ID und rufen zum Schluss diese Methode auf. Der Rückgabewert ist die Größe der Liste.

jclass listClass = (*env)->GetObjectClass(env, list);
jmethodID sizeMethod = (*env)->GetMethodID(env, listClass, "size", "()I");
int listSize = (*env)->CallIntMethod(env, list, sizeMethod);

Um an ein Element der Liste zu gelangen, benötigen Sie weiterhin die Methoden-ID der get(int i)-Methode:

jmethodID getMethod = (*env)->GetMethodID(env, listClass, "get", "(I)Ljava/lang/Object;");

Sie haben alles beisammen, was Sie zur Ausgabe benötigen. Geben wir zuerst über printf die intValue aus.

printf("C: intValue: %d\n", intValue);

Um die Liste auszugeben benötigen Sie eine Schleife, die so oft durchlaufen wird, wie die Liste Elemente besitzt. In dieser Schleife rufen Sie dann die get-Methode mit der aktuellen Position in der Schleife auf. Den Rückgabewert casten Sie in einen jstring. Anschließend wird er über die Methode GetStringUTFChars, die Sie bereits aus dem letzten Kapitel kennen, in eine C-Zeichenkette konvertiert. Diese kann dann über printf ausgegeben werden.

printf("C: printList reverse\n");
for (listSize = listSize - 1; listSize > -1; listSize--) {
  jstring cur = (jstring)(*env)->CallObjectMethod(env, list, getMethod, listSize);
  const char *c_cur = (*env)->GetStringUTFChars(env, cur, 0);
  printf("\tC: %s\n", c_cur);
}

Wenn Sie das Programm kompiliert und ausgeführt haben, sollten Sie folgende Ausgabe auf der Konsole erhalten:

Java: printList
        Java: Eintrag: 1
        Java: Eintrag: 2
        Java: Eintrag: 3
        Java: Eintrag: 4
        Java: Eintrag: 5
C: intValue: 5
C: printList reverse
        C: Eintrag: 5
        C: Eintrag: 4
        C: Eintrag: 3
        C: Eintrag: 2
        C: Eintrag: 1
Java: Ich wurde von C gesetzt!

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.