headlogo

JNI Tutorial

Falls nur C-Source-Code eines Programmes vorhanden ist, dieser aber über Java nutzbar gemacht werden soll, bietet sich in JNI eine Möglichkeit dazu. Die Idee dieses Tutorials ist ein "Durchstich" um das Prinzip zu verstehen und die Fallstricke zu kennen. In meinem Beispiel verwende ich Eclipse Helios und Java 6 unter Windows XP. Das Tutorial mit den Beispieldateien findet sich hier

Schritt 1: Installation Eclipse Helios und JDK 1.6 (update 22)

(diesen Schritt überspringe ich, da einfach bzw. ausreichend dokumentiert)

Schritt 2: Installation C-Umgebung

Damit unter Eclipse mit C entwickelt werden kann benötige ich:

  • das CDT-Plugin für Eclipse
  • MinGW-Libraries (unter Linux nicht notwendig)
    Note: Cygwin empfehle ich nicht, die JVM crashte bei Tests mit "ACCESS VIOLATION"-Meldungen
  • 2a: CDT-Plugin-Installation

  • 1. Lade das Zip herunter für Helios-version
  • 2. in Eclipse das zip unter "help -> install new software -> add -> archive" einbinden
  • 3. alles ankreuzen ausser "C/C++ Remote Launch" (sonst crasht Installation)
  • 2b: MinGW-Installation

    Folgende Packages habe ich manuell hier heruntergeladen:

  • binutils-2.22-1-mingw32-bin.tar.lzma
  • gcc-c++-4.6.2-1-mingw32-bin.tar.lzma
  • gcc-core-4.6.2-1-mingw32-bin.tar.lzma
  • gdb-7.4-2-mingw32-bin.tar.lzma
  • gmp-5.0.1-1-mingw32-dev.tar.lzma
  • libexpat-2.0.1-1-mingw32-dll-1.tar.gz
  • libgcc-4.6.2-1-mingw32-dll-1.tar.lzma
  • libgmp-5.0.1-1-mingw32-dll-10.tar.lzma
  • libgomp-4.6.2-1-mingw32-dll-1.tar.lzma
  • libmpc-0.8.1-1-mingw32-dll-2.tar.lzma
  • libmpfr-2.4.1-1-mingw32-dll-1.tar.lzma
  • libpthreadgc-2.9.0-mingw32-pre-20110507-2-dll-2.tar.lzma
  • libssp-4.6.2-1-mingw32-dll-0.tar.lzma
  • libstdc++-4.6.2-1-mingw32-dll-6.tar.lzma
  • make-3.82-5-mingw32-bin.tar.lzma
  • mingwrt-3.20-mingw32-dev.tar.gz
  • mingwrt-3.20-mingw32-dll.tar.gz
  • mpc-0.8.1-1-mingw32-dev.tar.lzma
  • mpfr-2.4.1-1-mingw32-dev.tar.lzma
  • pthreads-w32-2.9.0-mingw32-pre-20110507-2-dev.tar.lzma
  • w32api-3.17-2-mingw32-dev.tar.lzma
  • 1. Lade diese herunter und entpacke sie nach zB. "C:\mingw" (2x Entpacken nötig). Ein gutes Entpackertool ist "7 Zip"
  • 2. ergänze oder erzeuge neu eine Env-Variable mit dem Namen "PATH" und dem Wert "C:\mingw\bin" (= "bin"-Unterverzeichnis von MinGW)
    unter "Arbeitsplatz -> Eigenschaften -> erweitert -> Umgebungsvariablen"
  • 2c: Eclipse neu starten, Die Übung kann beginnen.

    Projekt 1: Java Projekt

    1.

    starte Eclipse, dann "file->new->project->java-project" nennen wir es "JniCall"

    2.

    kreiere im "src"-ordner neue Klasse "Main" (der Einfachheit halber machen wir keine Packages)

    3.

    folgender sourcecode eingeben:

    public class Main {
    
    
    	public static native void CLibSaysHello(); //Deklaration der C-Methode
    	
    	static //einmaliger Aufruf durch den ClassLoader der JVM zur Laufzeit
    	{
    		System.loadLibrary("libJniDLL");		
    	}
    	
    	public static void main(String[] args) {
    		System.out.println("java says hello");
    		CLibSaysHello();
    	}
    
    }
    

    4.

    kompilliere unter project -> Build Project

    5.

    Nun starte eine cmd-konsole und wechsle ins verzeichnis "Laufwerk:\my_Eclipse_Workspace\JniCall\bin". Es findet sich dort eine "Main.class"

    gib ein (je nach installation des JDK kann das Verzeichnis anders lauten):

    c:\Programme\Java\jdk1.6.0_22\bin\javah.exe -o jniHeader.h Main
    

    Das erzeugt ein C-Headerfile, das wie folgt aussieht:

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class Main */
    
    #ifndef _Included_Main
    #define _Included_Main
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     Main
     * Method:    CLibSaysHello
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_Main_CLibSaysHello
      (JNIEnv *, jclass);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    

    soweit, so gut, lassen wir Java ruhen und machen jetzt C

    Projekt 2: C Projekt

    1.

    Nun beginne ein C-projekt und zwar eine DLL:

  • "File -> New -> Project.. -> C/C++ -> C Project"
  • wähle im nächsten Reiter:

  • "Shared Library -> Empty Project"
  • "MinGW GCC"
  • "JniDLL" als Projektname
  • kreiere ausserdem einen source-folder mit Namen "src" im Projekt

  • "file -> new -> source-folder"
  • 2.

    jetzt folgen wichtige Projekteinstellungen:

  • JNI-includes hinzufügen: Unter "Project -> properties -> C/C++ Build -> Settings -> GCC C Compiler -> Includes -> Include Paths" folgende Werte hinzufügen(je nach installation anders):
    "C:/Programme/Java/jdk1.6.0_22/include/" sowie
    "C:/Programme/Java/jdk1.6.0_22/include/win32/"

  • Linker Flag hinzufügen : Trage unter "Project -> properties -> C/C++ Build -> Settings -> MinGW C Linker -> Miscellaneous -> Linker flags" den Wert ein:
    "-Wl,--kill-at"
    (Das ist nötig, weil sonst die DLL beim Kompillieren mit @-Zeichen angereichert wird, die Java nicht versteht. Unter Linux braucht es dieses Flag nicht)
  • Ändere das C-Headerfile "jni_md.h" im JDK-Verzeichnis (je nach Installation):
    "C:/Programme/Java/jdk1.6.0_22/include/win32/"

    ersetze die Zeile:
        typedef __int64 jlong;
    mit:
        #ifdef __GNUC__
        typedef long long jlong;
        #else
        typedef __int64 jlong;
        #endif
    
    Das ist nötig weil der GCC den Datentyp "__int64" nicht versteht und Error meldet
  • Damit sind die Einstellungen beendet und die Sources können kreiert werden:

    3.

    kopiere aus dem Java-Projekt das headerfile "jniHeader.h" in den source-folder

    4.

    kreiere ein source-file "main.c" im gleichen folder und implementiere folgenden Code

    #include "jniHeader.h"
    #include <stdio.h>
    
    JNIEXPORT void JNICALL Java_Main_CLibSaysHello
      (JNIEnv * env, jclass class)
    {
    	printf("C says Hello");
    }

    5.

    nun kann eclipse ohne Probleme kompillieren und kreiert ein File genannt "libJniDLL.dll"

    Zusammenführung von Projekt 1 und 2:

    kopiere diese ".dll" nun ins Projekt-Root-Verzeichnis des Java-Projekt "JniCall", also nach:
    "Laufwerk:\my_Eclipse_Workspace\JniCall\"

    (Es ist wichtig, dass die DLL in den richtigen Java Library-Path kopiert wird weil Eclipse diesen so definiert und die JVM zur Laufzeit dort die DLL sucht.)

    Starte nun Main.java mit "rechter Maustaste -> Run as -> Java Application" und geniesse den Konsolen-Output:

    java says hello
    C says Hello
    
    

    Jar-Projekt

    Nun wollen wir das ausserhalb von Eclipse laufen lassen.

    1.

    rechts-Klick im Projekt "JniCall"

  • "export -> Runnable Jar file"
  • wähle im nächsten Reiter:

  • Launch Configuration = Main - JniCall
  • Export destination = irgendwo
  • wähle "extract required libraries into generated JAR"
  • 2.

    kopiere "libJniDLL.dll" in den gleichen Ordner wo das Jar liegt

    3.

    öffne eine cmd-konsole und gib ein:

    java -jar jni.jar

    Voila!!

    Kompillieren und Testen unter Ubuntu:

    nachdem es nun unter Windows funktioniert nehmen wir Ubuntu. Das geht derart gut, dass wir nicht mal Eclipse dazu brauchen. Ich empfehle die C-Sources unter Linux zu kompillieren, da ein Cross-Compiling unter Windows keine Binaries im ELF-Format erzeugt

    Falls "openjdk" noch nicht installiert ist, öffne eine Shell und gib ein:

    sudo apt-get install openjdk-6-jre

    Während der Installation ist man vom Internetzugang abhängig wie ein Junkie von der Heroin-Spritze. Offline-Installationen sind in der Ubuntu-Welt leider immer noch kompliziert.

    1.

    Kopiere die Sources "Main.java", "jniHeader.h" und "main.c" an beliebigen Ort (zB /root/Arbeitsfläche/jni)

    starte eine shell und wechsle dahin

    cd /root/Arbeitsfläche/jni

    2.

    Kompillieren und Linken der C-Sources

    gib nacheinander folgende Kommandos ein:

    gcc -I/usr/lib/jvm/java-6-openjdk/include -I/usr/lib/jvm/java-6-openjdk/include/linux -O0 -g3 -Wall -c -fmessage-length=0 -omain.o main.c
    gcc -shared -oliblibJniDLL.so main.o

    Wichtiger Hinweis: Die JVM verwendet unter Linux ein "lib" als Such-Präfix und ".so" als Such-Suffix. Unter Linux wird also nach "liblibJniDLL.so" gesucht. Unter Windows wird analog nach "libJniDLL.dll" gesucht, also nur ein ".dll" als Such-Suffix

    3.

    Zum Kompillieren der Java-Klasse gib ein:

    javac Main.java

    4.

    benötigt werden nur "Main.class" und "liblibJniDLL.so". Teste nun mit

    java -Djava.library.path="./" Main

    Anmerkung: Die Djava-Option braucht es, damit die JVM das .so File im richtigen Pfad sucht und findet.

    Ich hoffe die Einführung hat Spass gemacht