Java Native Access

Martin Kompf

Java kann aufgrund seiner Plattformunabhängigkeit nicht alle Funktionen bereitstellen, die das jeweilige Betriebssystem einem C-Programmierer bietet. Mittels Java Native Access (JNA) lassen sie sich jedoch relativ einfach nachrüsten.

Einschränkungen wegen Portabilität

So hat Java zum Beispiel keine Funktionen zur Abfrage der Prozess-Id, (Unix-)Gruppenzugehörigkeit oder (Windows-)Volumeinformation. Ursächlich ist das in der Plattformunabhängigkeit von Java begründet, da es solche Funktionen nicht auf allen von Java unterstützten Betriebssystemen gibt oder sie eine unterschiedliche Semantik besitzen.

Um diese Informationen trotzdem in einem Javaprogramm verwenden zu können, muss man letztendlich nativen Code aufrufen. Dabei handelt es sich in der Regel um ein in C oder C++ geschriebenes und anschließend kompiliertes Modul, welches seinerseits Zugriff auf die vollständige C-API des Betriebssystems hat. Java bietet dafür das Java Native Interface (JNI) an. Sein Einsatz zieht allerdings zwangsläufig das Schreiben von C++ Code sowie die Verwendung eines C++ Compilers und Linkers und damit zusätzlichen Aufwand im Projekt nach sich.

Standardisierter Zugriff auf Standardbibliotheken

Wenn es allerdings nur darum geht, bereits vorhandene C-Funktionen von Java aus aufzurufen, kann man sich das Schreiben eigenen JNI-Codes meistens sparen. Voraussetzung dafür ist nur, dass die Funktion von einer dynamischen Library (DLL, Shared Object) exportiert wird. Die Aufrufschnittstelle einer DLL ist (pro Betriebssystem) standardisiert und somit kann man ebenfalls standardisierten Code schreiben, der die Brücke zwischen JNI und einer DLL bildet.

Java Native Access (JNA) stellt genau diese Brücke dar. Auf der Projektseite finden sich zwei JAR-Files, die man einfach in den Classpath seines eigenen Projektes aufnimmt. Die Datei jna-?-?.?.jar enthält neben den JNA-Basisklassen den nativen Code für BSD, Linux, Mac OS X, Solaris und Windows. In jna-platform-?.?.?.jar stehen Definitionen für die Benutzung einiger Standardbibliotheken, wie Win32 oder X11.

Hinweis für Ubuntu: Die JAR-Files von der JNA-Projektseite haben nicht unter Ubuntu 12.10 zusammen mit der OpenJDK für 64 bit funktioniert! Stattdessen sollte man hier auf die durch Ubuntu mitgelieferten JAR-Files zurückgreifen, die sich per sudo apt-get install libjna-java installieren lassen. Sie befinden sich dann im Verzeichnis /usr/share/java.

So enthält das Interface Win32 die Funktion GetCurrentProcessId zur Abfrage der Prozess-Id unter Windows. Die Ausgabe der Id des eigenen Prozesses in einem Javaprogramm erfolgt dann ganz einfach mittels:

import com.sun.jna.platform.win32.Kernel32;

public class JNADemo {

  private void printProcessId() {
    int pid = Kernel32.INSTANCE.GetCurrentProcessId();
    System.out.println("My Process id is " + pid);
  }

  // siehe unten
}

Leichte Erweiterbarkeit

Dieser Code ist nun natürlich nicht mehr portabel, er läuft zum Beispiel nicht auf einem Unix-System. Dort fragt man ja die Prozess-Id auch nicht mittels GetCurrentProcessId, sondern per getpid ab. Eine entsprechende Deklaration kann man leicht hinzufügen. Man packt sie in ein von com.sun.jna.Library abgeleitetes interface:

import com.sun.jna.*;

public interface CLibrary extends Library {

  public int getpid();
  
  // weitere Definitionen siehe unten

  CLibrary INSTANCE = (CLibrary) Native.loadLibrary("c", CLibrary.class);
}

Wichtig ist, dass Name, Parameterliste (die hier leer ist) und Rückgabetyp exakt mit der Definition von getpid in der Unix C-API übereinstimmen. Die Initialisierung der statischen Variable INSTANCE erledigt das Laden der Bibliothek c. JNA expandiert unter Unix automatisch den Namen c zur Shared Library libc.so. Siehe hierzu die Regeln für das Library Mapping.

Die Klasse JNADemo würde man dann vielleicht um eine Erkennung des Betriebssystems erweitern:

  private void printProcessIdUnix() {
    int pid = CLibrary.INSTANCE.getpid();
    System.out.println("My Process id is " + pid);
  }
  
  public static void main(String[] args) {
    JNADemo demo = new JNADemo();

    if (System.getProperty("os.name").startsWith("Windows")) {
      demo.printProcessId();
    } else {
      demo.printProcessIdUnix();
    }
  }

Komplexe Typen

Eine Erweiterung des Interface CLibrary soll Funktionen enthalten, die Informationen zur Gruppe des Benutzers zurückliefern. Als erstes wäre da getgid, welches die Id der aktuellen Gruppe des laufenden Prozesses liefert. Die Definition in CLibrary ist einfach:

  public int getgid();

Will man nun mit der numerischen Id weitere Informationen, wie den Gruppenname abfragen, so benötigt man getgrgid. Zunächst informiert man sich auf der man-Page über die exakte Syntax:

$ man getgrgid
GETGRNAM(3)                Linux Programmer's Manual               GETGRNAM(3)
...
   struct group *getgrgid(gid_t gid);
...
   The group structure is defined in <grp.h> as follows:

       struct group {
           char   *gr_name;       /* group name */
           char   *gr_passwd;     /* group password */
           gid_t   gr_gid;        /* group ID */
           char  **gr_mem;        /* group members */
       };

Die Funktion bekommt also die numerische Id als Parameter übergeben, als Resultat liefert sie eine struct zurück. Als JNA-Pendant zu struct group definiert man die Java-Klasse Group, welche com.sun.jna.Structure erweitert:

  class Group extends Structure {
    public String gr_name; /* group name */
    public String gr_passwd; /* group password */
    public int gr_gid; /* group ID */
    public Pointer gr_mem; /* group members */

    @Override
    protected List getFieldOrder() {
      return Arrays.asList(new String[] { "gr_name", "gr_passwd", "gr_gid", "gr_mem" });
    }
  }

  public Group getgrgid(int gid);  

Wichtig ist auch hier die exakte Übereinstimmung der Schreibweise der Felder der Structure. Außerdem muss die obligatorische Methode getFieldOrder die Reihenfolge der Felder zurückliefern, damit JNA die korrekte Zuordnung vornehmen kann. C-Zeichenketten vom Typ char* konvertiert JNA in den Java-Typ String. Besondere Aufmerksamkeit verdient das Feld gr_mem vom Typ char**. Da sein Speicher durch die C-Library und nicht durch den aufrufenden Prozess alloziert wird, behilft man sich hier mit der speziellen JNA-Klasse Pointer. Detaillierte Informationen hierzu liefert der Abschnitt Type Mapping in der JNA Dokumentation.

Nach dieser Vorarbeit könnte die Abfrage und Ausgabe von Gruppeninformationen unter Unix folgendermaßen aussehen:

  private void printGroupInformation() {
    int gid = CLibrary.INSTANCE.getgid();
    Group group = CLibrary.INSTANCE.getgrgid(gid);
    System.out.printf("Your real group is %s with id %d%n", group.gr_name, gid);
    String[] members = group.gr_mem.getStringArray(0);
    System.out.printf("Other members of this group are: %s%n", Arrays.asList(members));
  }

Speicher, Pointer und Referenzen

Im Gegensatz zu Java, welches Funktionsparameter stets per Value übergibt, kennt C auch die Parameterübergabe per Pointer und Reference. Damit kann die Parameterliste zur Rückgabe von Werten genutzt werden, man spart sich so die Definition einer zusätzlichen struct wie bei getgrgid. Prominenter Vertreter dieser Technologie ist die Win32 API. So ist zum Beispiel die Funktion GetVolumeInformationW zur Abfrage von Seriennummer und Namen von Volumes unter Windows folgendermaßen in der Win32 API dokumentiert:

BOOL WINAPI GetVolumeInformation(
  _In_opt_   LPCTSTR lpRootPathName,
  _Out_opt_  LPTSTR lpVolumeNameBuffer,
  _In_       DWORD nVolumeNameSize,
  _Out_opt_  LPDWORD lpVolumeSerialNumber,
  _Out_opt_  LPDWORD lpMaximumComponentLength,
  _Out_opt_  LPDWORD lpFileSystemFlags,
  _Out_opt_  LPTSTR lpFileSystemNameBuffer,
  _In_       DWORD nFileSystemNameSize
);

Während der Rückgabewert nur anzeigt, ob die Funktion fehlerfrei gelaufen ist, erfolgt die Rückgabe der Ergebnisse über Pointer in der Parameterliste. Den Speicher hierfür muss der aufrufenden Prozess reservieren. Bei den LPTSTR Pointern muss man der Funktion dann auch noch mit einem zusätzlichen Size Parameter mitteilen, wie viel Speicher man alloziert hat. Für den Umgang mit Pointern, Referenzen und Speicher stellt JNA entsprechende Hilfsklassen bereit. Die Deklaration der Java Methode GetVolumeInformationW sieht dann so aus:

import java.nio.*;

import com.sun.jna.*;
import com.sun.jna.platform.win32.*;

public interface Kernel32Ext extends Kernel32 {
  boolean GetVolumeInformationW(
      WString lpRootPathName,
      CharBuffer lpVolumeNameBuffer,
      WinDef.DWORD nVolumeNameSize,
      WinDef.DWORDByReference lpVolumeSerialNumber,
      WinDef.DWORDByReference lpMaximumComponentLength,
      WinDef.DWORDByReference lpFileSystemFlags,
      CharBuffer lpFileSystemNameBuffer,
      WinDef.DWORD nFileSystemNameSize
      );

  Kernel32Ext INSTANCE = (Kernel32Ext) Native.loadLibrary("kernel32", Kernel32Ext.class);
}

Zu beachten ist hier auch, dass GetVolumeInformationW mit Unicode Strings arbeitet, LPCTSTR ist daher nicht als String, sondern WString in Java abzubilden. Außerdem muss nun der Java Programmierer vor dem Aufruf der Funktion die Buffer und Reference Klassen explizit initialisieren:

  private void printVolumeInformation(String path) {
    final WinDef.DWORD bufSize = new WinDef.DWORD(512);
    CharBuffer volName = CharBuffer.allocate(bufSize.intValue());
    WinDef.DWORDByReference volSerNbr = new WinDef.DWORDByReference();
    boolean ok = Kernel32Ext.INSTANCE.GetVolumeInformationW(new WString(path),
        volName, bufSize, volSerNbr, null, null, null, new WinDef.DWORD(0));
    if (ok) {
      System.out.printf("%s has volume name %s and id %x%n", path,
          volName.toString().trim(), volSerNbr.getValue().longValue());
    }
  }

Fazit

JNA ist eine probate Methode, um existierenden C-Code bequem in Java nutzen zu können. Damit verliert das Javaprogramm ohne weitere Maßnahmen naturgemäß seine Portabilität; das zieht zusätzlichen Aufwand für das Testen auf verschiedenen Plattformen nach sich. Da das JNA-Projekt kompilierte Libraries für alle Mainstream Betriebssysteme bereitstellt, kommt man ohne den Betrieb einer nativen C oder C++ Buildumgebung zum Ziel. Allerdings sollte sich der Javaprogrammierer schon mit den Grundlagen der Programmierspache C, insbesondere Pointern, Referenzen und Speicherverwaltung, auskennen.