Nachdem ich nun den den LCD-Screen C-Berry Touch erfolgreich installieren konnte, möchte ich eigene Programme schreiben, die hiermit Ein- und Ausgaben durchführen. Die Programmentwicklung soll mit Java geschehen. Dies ist leider nicht direkt möglich. Es besteht jedoch die Möglichkeit, C-Bibliotheken anzubinden. Über diesen Weg können dann die zum C-Berry mitgelieferten C-Routinen zur Ansteuerung genutzt werden.

Um das Ganze ein wenig besser verständlich zu machen, soll beispielhaft aufgezeigt werden, wie eine einfache C-Funktion eingebunden werden kann. Diese Funktion macht nichts weiter, als einen Text auf der Konsole auszugeben und einen übergebenen Zähler zu erhöhen. Also etwa so etwas:

int ursPrint(char *text, int counter
{ printf(text, counter);
  return counter++;
}

Im Beispiel erfolgt die Entwicklung mit Eclipse (Version: Mars.1 Release (4.5.1)). Eclipse kompiliert die JAVA-Klassen direkt bei der Speicherung. Ein separater Kompiler-Schritt taucht deshalb in der folgenden Anleitung nicht auf.

Neben Eclipse muss das Java SE Development Kit installiert sein. Zur Entwicklung des Beispiels wurde das Java SE Development Kit 8u65 verwandt. Die Übersetzung des C-Programms erfolgt mit gcc. Die Übersetzung wird mit make automatisiert.

Projekt zum Download  Quellen und Klassen dieses Projekts zum Download

Projekt zum Download  Quellen und Klassen von URS-Utils zum Download

URS-Utils.jar zum Download (Binaries)  URS-Utils.jar zum Download (Binaries)

Dokumentation zu URS-Utils  Dokumentation zu URS-Utils

Hinweis: Absätze, die sich auf das Beispiel beziehen, besitzen diesen Rahmen.


In­halts­ver­zeich­nis

1. Prinzip
2. Entwicklungsreihenfolge
2.1. Schnittstelle festlegen
2.2 Java-Wrapper-Klasse schreiben
2.3 C-Header-File generieren
2.4 Dynamische Bibliothek in C entwickeln
2.5 Wrapper-Klasse mit dynamischer Bibliothek verbinden
3. Test-Applikation erstellen
3.1 Test-Applikation per Class Path
3.2 Test-Applikation mit eingebundenen JAR-Archiven


1. Prinzip

Die Einbindung der C-Routinen erfolgt über Java Native Interface (JNI). Das entsprechende Schichtenmodell ist in der folgenden Abbildung dargestellt.

JNI Schichtenmodell

Die Applikation ruft die C-Methoden über einer Wrapper-Klasse auf.  In dieser Klasse sind Methoden-Prototypen in Java-Notation hinterlegt.

Die Aufrufe werden über JNI an die C-Bibliothek weitergeleitet und zwar in einer Form, die C-kompatibel ist.

Die C-Funktion kann au die Übergabe-Parameter zugreifen, die vorgesehene Funktionalität ausführen und Werte zurückgeben. Die Rückgabe erfolgt dann in umgekehrter Reihenfolge.

Die dynamische Bibliothek wird nicht automatisch geladen. Dies geschieht über explizite Programmanweisungen.

Die Entwicklung der Bibliotheken und der Applikationen soll so weit als möglich mit Eclipse durchgeführt werden.

An dieser Stelle sei darauf hingewiesen, dass das Entwickeln einer solchen Bibliothek in Eclipse etwas trickreich ist, nicht was den Code betrifft, sondern was das Finden der Dateien betrifft. Hier muss man sich sehr genau an die Regeln halten. Hier mein Rezept.

Hinweis: Die Eigentliche Entwicklung ist für Windows beschrieben. Dies ist meiner persönlichen Vorliebe geschuldet. An vielen Stellen habe ich die entsprechenden Schritte für Linux hinzugefügt.


2. Entwicklungsreihenfolge

Die nachfolgend beschriebene Entwicklung der JAVA-Komponenten erfolgt mit Eclipse. Die Verwaltung von Verweisen ("Build Path") ist sehr vielschichtig und differiert deutlich je nach Projekt-Ansatz (einzelnes oder mehrere Projekte, gemeinsamer Workspace oder nicht). Darauf, dass der hier gezeigte Weg der beste ist, will ich mich nicht festlegen. Aber: Es hat funktioniert.

Im Endeffekt werden vier Projekte erstellt:

Projektstruktur

UrsSimpleNativePrint enthält die Wrapper-Klasse.

UrsSimpleNativePrintC enthält die C-Komponenten.

UrsTestSampleNativePrint ist ein Applikationsprojekt, dass Wrapper-Klasse über den Class Path einbindet. Dieses Projekt-Art eignet sich zum Test der Wrapper-Klasse, da Änderungen in dieser sofort wirksam werden.

UrsTestSampleNativePrintJar ist ein Applikationsprojekt, dass eine fertig entwickelte und als JAR-Archiv zur Verfügung stehenden dynamische Bibliothek verwendet.

2.1 Schnittstelle festlegen

Sinnvollerweise beginnt man damit, das Interface festzulegen. Werden bei diesem Schritt Fehler gemacht oder wichtige Dinge vergessen, müssen viele der folgenden Schritte wiederholt werden. D.h.

Nach Möglichkeit sollten für die Parameter und die Rückgabewerte native Typen genutzt werden. Das erleichtert die Entwicklung ungemein. Weiterhin sollten die aufgerufenen C-Funktionen möglichst elementar ausgelegt sein und soviel Programmlogik wie möglich in der Java-Umgebung abgewickelt werden. Hierdurch ist man deutlich flexibler.

Einerseits... Andererseits ist kompilierter C-Code natürlich schneller in der Ausführung als interpretierter Java-Code. Hier gilt also ein geeignetes Mittelmaß zwischen Performance und Flexibilität zu finden.

Im Beispiel ist die Schnittstelle recht trivial. Sie besteht aus einer einzelnen Methode, die einen String und einen Integer als Parameter übergibt und einen Integer zurück geliefert bekommt. Auf die Typenkompatibilität werde ich später eingehen.

2.2 Java-Wrapper-Klasse schreiben

Die Wrapper-Klasse stellt das Interface zur nativen Bibliothek dar. Hier werden in Java-Notation die Prototypen für die in C zu implementierenden Funktionen angegeben. Über diese Prototypen können dann andere Java-Funktionen mit den C-Funktionen kommunizieren. In wie weit man die C-Funktionen direkt sichtbar macht oder eine besser weitere Schichten einzieht, z.B. zur Parameterprüfung, hängt von den Umständen ab.

Die Funktionsprototypen haben den zusätzlichen Zugriffsmodifizierer native. Die anderen Modifizierer (z.B. public, static) können wie gewohnt verwandt werden.

In wie weit weitere Programmlogik in diese Klasse gepackt wird, hängt davon ab, wie universell diese Klasse genutzt werden soll. Es bietet sich an, nur hier zusätzlich die Funktionalitäten zu implementieren, die wichtig für den Betrieb der C-Funktionen sind, aber nicht notwendigerweise in der C-Umgebung entwickelt werden sollen. Ein typisches Beispiel wäre die Kombination aus Cursor-Positionierung und Textausgabe, die in zwei unterschiedlichen C-Funktionen implementiert sind, aber als gemeinsame Java-Methode angeboten werden werden sollen.

Eine minimalistisch ausgestattete Wrapper-Klasse wäre die folgende.

public class NativePrint {
    public static native int ursPrint(String Text, int Counter);
}

Sinnvoll ist es, in dieser Klasse eine Methode zu implementieren, die das Laden der nativen Bibliothek übernimmt. Die geschieht praktischerweise innerhalb eines statischen Initialisierers. Um dies zu vereinfachen kann man die Wrapper-Klasse von der Klasse NativeUtils im Package urs.utils ableiten (Download-Link am Anfang der Seite). Diese Klasse stellt passende Methoden bereit. Ein Verweis auf die Klassen muss in den Build Path eingetragen werden.

urs.utils einbinden
urs.utils einbinden.

So ausgerüstet stellt sich die Klasse wir folgt dar:

public class NativePrint {
    /**
     * Speichert der Namen der Temp-Datei.
     */
    private static String TempFileName;

    /**
     * Lädt die native Bibliothek
     */
    static {
        TempFileName = NativeUtils.tryLoadLibrary("NativePrint", NativePrint.class);
    }

    /**
     * Versucht die Temp-Datei zu löschen.
     */
    protected void finalize() {
        NativeUtils.removeLibrary(TempFileName);
    }

    /**
     * Gibt den angegebenen Text auf der Konsole aus und erhöht den Zähler um 1.
     * @param Text Auszugebender Text
     * @param Counter Zu erhöhender Zähler
     * @return Der erhöhte Zähler
     */
    public static native int ursPrint(String Text, int Counter);
}

Die zugehörige Eclipse-Projekt-Struktur sieht wie folgt aus:

Eclipse-Projet-Struktur

Das Projekt heißt UrsSimpleNativePrint. Die oben aufgeführte Klasse NativePrint befindet sich im Package urs.nativeprint. Eclipse legt die entsprechenden Verzeichnisse selbständig an.

2.3 C-Header-File generieren

Mit Hilfe das Programms javah wird eine zur Wrapper-Klasse passende Header-Datei erzeugt, die C-Prototypen für alle in der Wrapper-Klasse enthaltenen nativen Methoden generiert.

Die Erzeugung der Header-Datei erfolgt vom bin-Verzeichnis des Projekts aus. Die Pfadangaben erfolgen in der Package-Notation also mit Punkten als Trenner für die einzelnen Elemente (s. Beispiel).

Im Beispiel erstellt das Kommando

...\UrsSimpleNativePrint\bin>javah urs.nativeprint.NativePrint

die Header-Datei urs_nativeprint_NativePrint.h mit folgendem Inhalt:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class urs_nativeprint_NativePrint */

#ifndef _Included_urs_nativeprint_NativePrint
#define _Included_urs_nativeprint_NativePrint
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     urs_nativeprint_NativePrint
 * Method:    ursPrint
 * Signature: (Ljava/lang/String;I)I
 */
JNIEXPORT jint JNICALL Java_urs_nativeprint_NativePrint_ursPrint
  (JNIEnv *, jclass, jstring, jint);

#ifdef __cplusplus
}
#endif
#endif

Die Datei enthält einen Prototypen für die JAVA-Methode ursPrint mit dem Namen Java_urs_nativeprint_NativePrint_ursPrint.

2.4 Dynamische Bibliothek in C entwickeln

Hier geht es darum, die Prototypen der Methoden aus der generierten Header-Datei mit Leben zu füllen, zu kompilieren und zu einer dynamischen Bibliothek zu binden.

Projekt für die C-Entwicklung anlegen

Im Linux-Umfeld hat Eclipse die Verzeichnisse regelmäßig "aufgeräumt" und die C-Dateien aus dem bin-Verzeichnis gelöscht. Wahrscheinlich kann man Eclipse so konfigurieren, dass dies nicht passiert.

Für mich ist es auch einfacher, den späteren Kompilierungsvorgang der C-Dateien per Hand vorzunehmen anstatt Eclipse hierfür einzurichten. Ich bin deshalb den Weg des geringsten Widerstandes gegangen und habe einen eigenständiges, vom Typ her unspezifiziertes Projekt für die C-Entwicklung angelegt und die erzeugte Header-Datei dorthin verschoben.

Unspezifizierter Projekt-Typ:
Auswahl Projekt-Typ

 Der Projektname ist UrsSimpleNativePrintC. Die Projektstruktur sieht also nun wie folgt aus:

Erweiterte Projket-Struktur

Codieren der C-Funktionen

Im gleichen Verzeichnis, in dem die Header-Datei gelandet ist, erstellt man nun eine C-Datei, die die notwendigen Methoden implementiert.

#include "urs_nativeprint_NativePrint.h"

#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jint JNICALL Java_urs_nativeprint_NativePrint_ursPrint
  (JNIEnv *env, jclass this, jstring jText, jint jCounter)
{ 
    const char *str = (*env)->GetStringUTFChars(env, jText, 0);

    printf(str, jCounter);
    
    (*env)->ReleaseStringUTFChars(env, jText, str);
    
    return jCounter++;
}

#ifdef __cplusplus
}
#endif

Die Datentypen für die Übergabe-Parameter sind in der Oracle-Java-Dokumentation im Abschnitt JNI Types and Data Structures zu finden. Referenztypen, wie z.B. String, müssen erst gewandelt werden, damit auf sie von C aus zugegriffen werden kann. Eventuell hierfür bereit gestellte Puffer müssen wieder freigegeben werden. Für Strings sind dies die Methoden GetStringUTFChars und ReleaseStringUTFChars.

Kompilieren der C-Bibliothek

 Benötigt wird eine dynamische Bibliothek (Shared Library im Linux-Umfeld, DLL bei Windows). Die Namenskonventionen für die Dateinamen sind "lib<BibName>.so" bei Linux und "<BibName>.dll" bei Windows.

Linux:

Für das Kompilieren der Bibliothek benutze ich gcc. Unter Linux sieht der Kompiliervorgang wie folgt aus:

# Include-Pfad festlegen (ggf. anpassen!)
export JAVA_HOME=/usr/lib/jvm/java-7-openjdk-armhf
# Programm kompilieren (XXXX durch Dateinamen ersetzen)
gcc -fPIC -c XXXX.c -I $JAVA_HOME/include -I $JAVA_HOME/include/linux
# Bibliothek binden (XXXX durch Dateinamen ersetzen)
gcc XXXX.o -shared -o libXXXX.so -Wl,-soname,XXXX

Das Kommando export sorgt dafür, dass die Variable nicht nur in der aktuellen Shell, sondern auch in den von Ihr aufgerufenen Programmen zur Verfügung steht .

Die "-fPIC"-Option beim Compiler-Aufruf sorgt für die Erzeugung von positionsunabhängigem Code. Dieser Code eignet sich für dynamische Verknüpfungen unter Vermeidung jeglicher Beschränkung für die Größe der globalen Offset-Tabelle (siehe  ggc.gnu.org).

libXXXX.so ist der Dateiname XXXX (nach -soname) ist der Bibliotheksname.

Windows:

Unter Windows wird die Bibliothek durch folgende Kommandofolge erstellt:

# Include-Pfad festlegen (ggf. anpassen!)
set JAVA_HOME=C:\Program Files\Java\jdk1.8.0_65
# Programm kompilieren (XXXX durch Dateinamen ersetzen)
gcc -c XXXX.c -I "%JAVA_HOME%\include" -I "%JAVA_HOME%\include\win32"
# Bibliothek binden (XXXX durch Dateinamen ersetzen)
gcc XXXX.o -shared -o XXXX.dll -Wl,-soname,XXXX

Das Kommando set legt eine Umgebungsvariable mit dem Pfad auf das JAVA-Verzeichnis an

XXXX.dll ist der Dateiname XXXX (nach -soname) ist der Bibliotheksname.

Da man wahrscheinlich mehrere Versuche braucht, bis alle Fehler eliminiert sind, lohnt es sich den Vorgang über make wiederholbar zu machen.

Die makefile für Windows sieht wie folgt aus:

all: NativePrint.dll

JAVA_HOME=C:\Program Files\Java\jdk1.8.0_65
objects = urs_nativeprint_NativePrint.o

NativePrint.dll: urs_nativeprint_NativePrint.o
    gcc $(objects) -shared -o NativePrint.dll -Wl,-soname,NativePrint

urs_nativeprint_NativePrint.o: urs_nativeprint_NativePrint.c urs_nativeprint_NativePrint.h
    gcc -c urs_nativeprint_NativePrint.c -I "$(JAVA_HOME)\include" -I "$(JAVA_HOME)\include\win32"

clean:
    del *.o
    del *.dll

Das Verzeichnis hat nun folgenden Inhalt:

Dynamische Bibliothek

2.5 Wrapper-Klasse mit dynamischer Bibliothek verbinden

Je nachdem, wie die Einbindung der nativen Bibliothek in ein späteres Projekt geschehen soll, macht es Sinn, im zugehörigen Projekt einen Verweis auf die native Bibliothek einzutragen.

Verweis zur nativen Bibliothek


3. Test-Applikation erstellen

Bei der Einbindung der Bibliothek in eine Applikation gibt es es unterschiedliche Optionen, wie der Zugriff auf die verschiedenen Komponenten erfolgt. Eclipse bietet die Möglichkeiten, dies auf Projekt-Ebene zu tun oder Komponenten über den Class Path einzubinden oder sie über JAR-Archive zur Verfügung zu stellen. Man kann dies für jede Komponente separat entscheiden.

Die Einbindung auf Projekt-Ebene ist am unkompliziertesten und eignet sich gut, wenn durch die native Bibliothek eine projektspezifische Aufgabe erledigt werden soll. Eclipse regelt hier viele Dinge selbständig, die ansonsten separat eingestellt werden müssen. Diese Methode aber ungeeignet, wenn man generelle Funktionen für verschiedene Vorhaben entwickeln will.

 Die Nutzung von JAR-Archiven bietet den Vorteil, dass man in diesen Archiven die Komponenten en Bloc zur Verfügung stellen kann, aber immer nur die Version zur Verfügung steht, die zum Zeitpunkt der Archiv-Erstellung gültig war.

In der ersten Variante werden alle Komponenten über den Class Path eingebunden, in der zweiten erfolgt die Bereitstellung über sukzessiv aufgebaute JAR-Archive.

3.1 Test-Applikation per Class Path

Für die Testapplikation wird in Eclipse ein eigenständiges JAVA-Projekt UrsTestSimpleNativePrint angelegt. Zunächst gilt es, den Build Path einzustellen, damit dem Projekt die Verweise auf die Komponenten zur Verfügung stehen.

Build Path einstellen

Primär wird ein Verweis auf  die Wrapper-Klasse NativePrint benötigt. Der zugehöre Class Path wurde workspace-relativ über "Add Class Folder" eingefügt. Es ist das "bin"-Verzeichnis auszuwählen. Unterverzeichnisse werden über den Package-Namen selbständig ausgewählt.

NativePrint benötigt einen Verweis auf das Verzeichnis mit der übersetzten C-Bibliothek. Dies muss bei "Native library location" eingetragen werden. Weiterhin werden von NativePrint Methoden aus NativeUtils aufgerufen, so dass auch ein Verweis auf diese Bibliothek notwendig ist.

Der Code für die Test-Applikation ist relativ überschaubar:

import urs.nativeprint.NativePrint;

public class TestSimpleNativePrint {

    public static void main(String[] args) {
        System.out.println("Programm gestartet");
        
        int Counter = 47;
        Counter = NativePrint.ursPrint("Hallo %i\n", Counter);
        System.out.println("Das Ergebnis ist " + Counter);
    }
}

Der Output ist:

Programm gestartet
Das Ergebnis ist 48
Hallo 47

 Der Vollständigkeit halber, die Projektstruktur:

Struktur des Test-Projekts

Als letzter Schritt wird das gesamte Projekt zu einem ausführbarem JAR-Archiv gebunden. Die C-Bibliothek bindet Eclipse leider nicht automatisch mit ein. Sie muss manuell hinzugefügt werden und gehört ins Wurzel-Verzeichnis:

Struktur des JAR-Archivs

Als letzter Test dann die Ausführung dieses Archivs im Konsolenfenster:

Test im Konsolenfenster

3.2 Test-Applikation mit eingebundenen JAR-Archiven

Ist die Bibliothek ausgetestet und man möchte sie in weiteren Projekten verwenden, macht es Sinn, alles zu einem JAR-Archiv zu packen. Im ersten Schritt wird das Projekt mit der Wrapper-Klasse in ein "JAR file" exportiert. Die NativePrint.dll und die Klassen aus urs.utils müssen manuell hinzugefügt werden.

Das JAR-Archiv hat damit folgenden Aufbau:

Struktur des JAR-Archivs für die Bibliothek

Bei der Test-Applikation hat nun nur noch einen Verweis auf dieses Archiv eintragen:

Struktur des Test-Projekt bei Benutzung des JAR-Archivs

Ein aus diesem Projekt exportiertes "Runnable JAR file" enthält alle für die Ausführung notwendigen Komponenten.

Download-Link am Anfang der Seite