Heise 13.05.2026
09:26 Uhr

C-Libraries in Java nutzen 2: Funktionen mit veränderlichen Parametern


Die Foreign Function & Memory API bietet in Java einen deutlich einfacheren Zugang zu Funktionen in C-Libraries als das veraltete JNI.

C-Libraries in Java nutzen 2: Funktionen mit veränderlichen Parametern

Javas Foreign Function & Memory API (FFM) dient dazu, auf Code in einer Shared Library beziehungsweise DLL zuzugreifen, der in einer Programmiersprache wie C oder Rust geschrieben ist. Allerdings muss der Code dazu einige Voraussetzungen erfüllen. Diese dreiteilige Artikelserie zeigt anhand einer in C geschriebenen Demo-Library, wie eine Java-Anwendung die Funktionen der Bibliothek aufruft, welche Vorbereitungen erforderlich sind und welche Regeln zu beachten sind.

Nachdem der erste Teil gezeigt hat, wie man in Java eine in C geschriebene Shared Library lädt und einfache Funktionen dieser Shared Library aufruft, geht es jetzt um komplexere Szenarien. Er zeigt, wie man aus Java Funktionen mit veränderbaren Parametern aufrufen und Arrays sowie Strukturen übergeben kann.

Die bisherigen Beispiele haben die Aufrufe der nativen Funktionen einfach gehalten. Die Java-Anwendung hat lediglich Parameter durchgereicht und den Rückgabewert übernommen.

Anders sieht es bei den nächsten Beispielen aus. Als erstes folgt die einfache C-Funktion getVersion2, die wie die Funktion getVersion aus Teil 1 die Version der Library ermittelt. Die neue Funktion gibt die Versionsnummer aber nicht als Wert zurück, sondern verändert dazu einen Parameter. Das funktioniert in C, indem eine Anwendung für einen Parameter nicht den Wert selbst, sondern dessen Adresse übergibt (Call by Reference). Dieses Konstrukt sieht in C folgendermaßen aus:

Folgender Java-Code ruft die Funktion auf:

Das & kennzeichnet in C, dass die Funktion die Adresse der Variablen nutzt. Java erlaubt das Vorgehen nicht, sodass ein Rückgabewert unerlässlich ist. Folgende Java-Methode verwendet die C-Funktion mit Referenz:

Zuerst ruft der Code wie im ersten Teil der Serie wieder die Methode getMethodHandle() auf. Der Aufruf definiert den FunctionDescriptor für die Funktion getVersion2().

Die Angabe ValueLayout.ADDRESS für den Parameter zeigt an, dass die C-Funktion eine Adresse erwartet.

Jetzt kommt der spannendere Teil: Um eine Adresse übergeben zu können, muss die Java-Anwendung mittels der FFM-API einen Speicherbereich von vier Byte (für den Datentyp int) reservieren. Das geschieht mit einer Arena, die er erste Teil bereits erläutert hat. Das Erzeugen der Arena mit dem try-with-ressources-Statement stellt sicher, dass die Arena nach dem try-Block automatisch geschlossen und der darin verwaltete Speicher automatisch freigegeben wird. Es gibt verschiedene Typen von Arenas – die im Beispiel über ofConfined erzeugte sorgt dafür, dass die Anwendung nur auf Speicher des aktuellen Thread zugreifen kann. Eine mit ofConfined() erzeugte Arena – beziehungsweise der damit allozierte Speicher – ist daher nicht threadsicher.

Als nächstes gilt es, den erforderlichen Speicherbereich für den Parameter version zu allokieren. Dafür besitzt die Arena die Methode allocate(). Die Größe des benötigten Speichers kann man durch die Funktion byteSize() für die Variable ermitteln. Hier sei nochmals darauf hingewiesen, dass der Wert die Größe des Java-Datentyps darstellt und nicht zwingend etwas über den C-Datentyp aussagt. Da die C-Funktion einen int-Parameter entgegennimmt, sind wir auf der sicheren Seite, da int in C stets vier Byte umfasst. Bei einem long-Wert in C hängt die Größe dagegen von der Plattform ab.

Der Speicherbereich wird durch ein MemorySegment dargestellt, das beim Aufruf der Methode invoke an die C-Funktion weitergereicht werden muss.

Anschließend kann die Anwendung das Ergebnis auslesen. Dazu ruft sie auf dem MemorySegment die Funktion get auf und übergibt ihr das Layout des Speichers (in diesem Fall ein JAVA_INT) und den Offset zum Lesen aus dem MemorySegment. Für das Beispiel ist der Offset null. Durch die Angabe JAVA_INT gibt die Funktion einen int-Wert zurück, den die Anwendung weiterverarbeiten kann.

Die nächste Aufgabe baut auf dem Vorgehen auf, verarbeitet aber nicht nur einen Wert, sondern ermittelt den Durchschnitt aus einer Liste von int-Werten. Dazu muss sie der nativen Funktion ein Array von int-Werten übergeben:

Zunächst berechnet der Code die gesamte Speichergröße des Arrays (totalSize) und reserviert den benötigten Speicher mit allocate(). Anschließend belegt der Code den Speicher mit der Methode setAtIndex für das jeweilige MemorySegment. Der Aufruf erfolgt für jedes Element des Arrays.

Schließlich ruft der Code die Methode invoke für den MethodHandle auf und übergibt ihr als Parameter das Array und dessen Länge. Schließlich gibt sie das Ergebnis der C-Funktion zurück.