Der Brückenschlag zur Insel

Simply JNI

Mike Werner
©Shutterstock/kostin77

Java ist eine sehr angenehme und leicht zu erlernende Programmiersprache. Unangenehm wird es für den Entwickler, wenn er diese Insel verlassen muss, um seine Java-Anwendung mit der Außenwelt zu verbinden. Dies geschieht i.d.R. mittels Java Native Interface (JNI). Nicht der native Code selbst ist das Problem, sondern JNI als solches. Dieser Artikel zeigt wie einfach JNI sein kann, wenn das richtige Werkzeug zur Verfügung steht.

Aufgrund der deutlich effizienten Entwicklung und seiner Plattformunabhängigkeit, gewinnt Java zunehmend im Embedded- und Echtzeitumfeld an Bedeutung. Das Ökosystem Java ist allerdings nur so gut, wie die Dienste und Frameworks, die zur Verfügung stehen. Diese decken zwar fast jedes Bedürfnis, doch gerade wenn es gilt hardware-nahe Funktionen oder native Bibliotheken in eine Anwendung zu integrieren, erfährt man recht schnell ein Wechselbad der Gefühle. Solange man sich in der Java-Welt bewegt, erscheint alles recht unproblematisch, doch wechselt man auf die native Seite “der Macht” ist Frustration das vorherrschende Gefühl. Als wäre es nicht genug, dass die zur Verfügung stehenden Programmiersprachen und Werkzeuge in Sachen Komfort, mit den gewohnten in keinster Weise konkurrieren können, die Technologie zur Verschmelzung nativer Anwendungsteile mit jenen in Java, könnte nicht umständlicher und fehleranfälliger sein. Sie wird im Hause Oracle “Java Native Interface” (JNI) genannt. Sie stellt ein natives Framework zur Verfügung, mittels dem Java-Code aufgerufen, Objekte manipuliert, sowie Java- in C-Datentypen und vice versa konvertiert werden können. Das folgende Beispiel soll dies verdeutlichen:

public class Main {

	private final static Logger logger = Logger.getLogger("JNILogger");

	public static void main(String[] args) {

	logger.info("!!! Hello world !!!");

	}

}

 

Das kleine Programm demonstriert, wie einfach man mittels dem “java.util.logging” Framework, das seit Java 1.4 fester Bestandteil jedes JREs ist, Nachrichten loggen kann. So ein mächtiges Feature wäre gerade auch in nativen Code-Teilen hilfreich. Doch leider gibt es nichts Vergleichbares. Allerdings wäre es durchaus sinnvoll dieses Framework von der nativen Seite aus zu nutzen. Dazu erweitern wir einfach den Source-Code aus obigem Listing um eine native Methode. Dieser wird ein Text übergeben, der per “java.util.logging” geloggt werden soll. Der Quellcode sieht dann wie folgt aus:

public class Main {

    private final static Logger logger = Logger.getLogger("JNILogger");

    static{
        // native Bibliothek nach dem Laden der Klasse ebenfalls laden.
        System.loadLibrary("jnilogger");
    }

    private static native void log(String message);

    public static void main(String[] args) {

        logger.info("!!! Hello world !!!");

        log("!!! Hello world from the native side 🙂 !!!");
    }
}

Spannend ist nun die Implementierung der nativen Methode. Das folgende Listing ist aus Gründen der Übersichtlichkeit, von unten nach oben zu lesen:

/**
* Deklaration benötigter Klassen- und Methodennamen, sowie Methoden Signaturen.
*/
#define Logger_Class_Name "java/util/logging/Logger"
#define Logger_Method_getLogger_Name "getLogger"
#define Logger_Method_getLogger_Signature "(Ljava/lang/String;)Ljava/util/logging/Logger;"
#define Logger_Method_info_Name "info"
#define Logger_Method_info_Signature "(Ljava/lang/String;)V"

static jobject CallStaticObjectMethod(JNIEnv* env, const char* className, 
const char* name, const char* signature, ...){ va_list parameters; jobject result = NULL; jclass clazz = NULL; jmethodID methodId = NULL; va_start(parameters, signature); // Logger Klasse laden clazz = (*env)->FindClass(env, className); if (clazz == NULL ) { /* exception wurde ausgelöst */ return; } // Methoden ID holen methodId = (*env)->GetStaticMethodID(env, clazz, name, signature); if (methodId == NULL ) { /* exception wurde ausgelöst */ return; } // Methode aufrufen result = (*env)->CallStaticObjectMethodV(env, clazz, methodId, parameters); va_end(parameters); return result; } static void CallVoidMethod(JNIEnv* env, jobject target, const char* name, const char* signature, ...){ va_list parameters; jclass clazz = NULL; jmethodId = NULL; va_start(parameters, signature); // Klasse des Objektes laden clazz = (*env)->GetObjectClass(env, target); if (clazz == NULL ) { /* exception wurde ausgelöst */ return; } // Methoden ID holen id = (*env)->GetMethodID(env, clazz, name, signature); // Methode aufrufen (*env)->CallVoidMethodV(env, target, methodId, parameters); va_end(parameters); } static jobject GetLogger(JNIEnv *env, jstring name){ return CallStaticObjectMethod
(env, Logger_Class_Name, Logger_Method_getLogger_Name, Logger_Method_getLogger_Signature, name); } static void LogInfo(JNIEnv *env, jobject logger, jstring message){ return CallVoidMethod(env, logger, Logger_Method_info_Name, Logger_Method_info_Signature, message); } JNIEXPORT void JNICALL Java_Main_log(JNIEnv *env, jclass clazz, jstring message) { // Nativen String in Java String konvertieren. jstring name = (*env)->NewStringUTF(env, "JNILogger"); // Logger object holen jobject logger = GetLogger(env, name); // Mit Level info loggen LogInfo(env, logger, message); }

Dieses kleine Beispiel verdeutlicht sehr anschaulich, wie unangenehm die Entwicklung nativer Anwendungsteile ist. Um eine Java-Methode aufrufen zu können, muss zunächst die entsprechende Klasse geladen werden. Anschließend muss die ID der Methode, die aufgerufen werden soll, ermittelt werden. Evtl. sind noch Parameter in das entsprechende Zielformat zu konvertieren, sodass abschließend die Methode aufgerufen werden kann.
JNI-Code bringt nicht nur deutlich mehr Entwicklungsaufwand mit sich, sondern ist bei umfangreicheren Anwendungen, auch sehr schwer zu pflegen. Allein wenn Klassennamen geändert werden, müssen “define” Deklarationen und u.U. auch Methodennamen angepasst werden. Ändern sich Methodennamen und Signaturen, muss ebenfalls penibel geprüft werden, ob dies Auswirkungen auf Deklarationen, etc. hat. Bei derart unübersichtlichem Code, hat man schnell mal eine Deklaration übersehen, was in aller Regel zu einer RuntimeException führt. Exceptions liefern zwar einen StackTrace, der unerlässlich bei der Fehlersuche ist, doch endet dieser an der Grenze zur nativen Seite. Somit wird es schwierig, Fehler in komplexen Methoden aufzuspüren. Mal eben “rein-debuggen” ist bei nativem Code nicht so einfach und angenehm wie in Java.
Wir haben uns intensiv mit dem Thema auseinander gesetzt und einen Java-to-JNI Code-Generator entwickelt. Dieser steht Entwicklern als ANT-Task sowie als Maven-Plugin zu Verfügung. Die Verwendung ist denkbar einfach. In dem entsprechenden Build-Script sind lediglich die Java-Klassen anzugeben, für die eine native Repräsentation generiert werden soll. Genauer gesagt wird für sämtliche Konstruktoren, Methoden und Felder einer Klasse C-Code generiert, der diese für native Anwendungsteile verfügbar macht.
Generiert man für die Klasse java.util.logging.Logger C-Code, sieht das C-Beispiel von eben schon fast sympathisch aus:

JNIEXPORT void JNICALL Java_Main_log(JNIEnv *env, jclass clazz, jstring message) {
	// Nativen string in Java String konvertieren
	jstring name = (*env)->NewStringUTF(env, "JNILogger");

	// Methode Logger.getLogger(String) aufrufen
	jobject logger = java_util_logging_Logger.method.getLogger.invoke(env, name);

	// Methode Logger.info(String) auf Logger Objekt aufrufen
	java_util_logging_Logger.method.info.invoke(env, logger, message);
}

Getreu dem Motto “In Java ist alles ein Dreizeiler” kann man nun nahezu ebenso effizient Java-Code auf der nativen Seite nutzen.

Aufmacherbild: stock_Andrew pedestrian bridge in Moscow von Shutterstock / Urheberrecht: kostin77

Geschrieben von
Mike Werner
Mike Werner
M.Sc. Dipl.-Inf. Mike Werner ist Gründer und Geschäftsführer der brainchild GmbH, die ihren Hauptsitz im beschaulichen Niederbayern hat. Nach dem er acht Jahre in Forschungs- und Entwicklungsprojekten mit dem Schwerpunkt militärische Luftfahrt verbracht hat, gründete er die brainchild GmbH, um mit einer schlagkräftigen Mannschaft, innovative Projekte, Lösungen und Produkte zu schaffen. brainchild selbst ist ein Engineering Dienstleistungsunternehmen, welches sich unter Anderem sehr stark in den Bereichen Systementwicklung, Usability Engineering und IT-Security engagiert.
Kommentare
  1. Landei2014-02-28 13:54:34

    Wo ist der Vorteil gegenüber JNA ( https://github.com/twall/jna )?

    1. Mike Werner2014-03-10 19:35:51

      JNA ist etwas völlig anderes. Es dient dazu, von Java aus, "einfach" auf nativen Code zuzugreifen. Der vorgestellte Generator generiert allerdings den Code, um aus der nativen Welt auf Java-Code zuzugreifen. Dies ist z.B. dann nötig, wenn native Routinen von Java aus aufgerufen werden und diese komplexe Java-Objektstrukturen zurückgeben sollen, die ausschließlich auf der nativen Seite erstellt werden können. So etwas ist z.B. häufig bei der Anbindung von Treibern, bzw. Hardware zu finden.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.