Foreign Functions и Memory API

Последнее обновление: 21.12.2025

Хотя подход подключения функционала на Си в программу на Java с помощью JNI продолжает работать и в самых последних версиях Java, тем не менее подключение нативного кода с помощью JNI довольно трудоемко, требует написания дополнительного кода на Си, компиляцию соответствующей библиотеки и т.д. Чтобы упростить всю эту работу в последних версиях Java был добавлен новый API для доступа к "внешним" функциям и памяти - Foreign Functions and Memory API (FFM), ранее развивавшийся по именем Project Panama. Начиная с версии Java 14 этот API был экспериментальным, а в Java 22 данный API достиг состояния релиза и стал полноценной функциональностью платформы. Предполагается, что со временем он вытеснит JNI.

Для примера опять же используем стандартную библиотечную функцию printf из стандартной библиотеки языка Си. И для этого определим файл Program.java со следующим кодом:

import java.lang.invoke.*;
import java.lang.foreign.*;

public class Program {

    public static void main(String[] args) throws Throwable {

        Linker linker = Linker.nativeLinker();
        MethodHandle printf = linker.downcallHandle(
            linker.defaultLookup().findOrThrow("printf"),  // получаем функцию printf
            FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS));

        try (Arena arena = Arena.ofConfined()) {
            MemorySegment str = arena.allocateFrom("Hello METANIT.COM\n");
            int result = (int) printf.invoke(str);  // получаем результат из нативной функции
            System.out.printf("Напечатано символов %d\n", result);
        }
    }
}

Разберем, что происходит в коде. Прежде всего создаем линкер (компоновщик) для работы с нативными функциями:

Linker linker = Linker.nativeLinker();

Линкер по сути представляет связующее звено между Java и нативной архитектурой текущей операционной системы.

Далее определяем дескриптор для вызываемыой функции в виде объекта MethodHandle

MethodHandle printf = linker.downcallHandle(...)

MethodHandle printf представляет своего рода указатель на нативную функцию, который Java может вызвать как обычный метод.

Для создания этого объекта применяется метод linker.downcallHandle(), который принимает два параметра:

  • Дескриптор внешней функции. Чтобы найти эту функцию, вызывается последовательность методов:

    linker.defaultLookup().findOrThrow("printf")

    Здесь выражение linker.defaultLookup() выполняет поиск символов (функций) в стандартных библиотеках C, которые загружены по умолчанию. А выражение findOrThrow("printf") указывает имя искомой функции (то есть в нашем случае printf)

  • Параметры линкера. Применяются для описания сигнатуры функции

    FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)
    

    В данном случае указываем, что printf() возвращает int (ValueLayout.JAVA_INT) и принимает указатель - ValueLayout.ADDRESS (char*).

Следующий важный момент - выделение памяти для строки, которая будет передаваться в вызываемую функцию в качестве параметра:

try (Arena arena = Arena.ofConfined()) {
       MemorySegment str = arena.allocateFrom("Hello METANIT.COM\n");

Поскольку нативные функции не умеют работать с объектами Java (такими как String), данные нужно разместить в "неуправляемой" памяти (вне кучи Java), для чего применяются следующие действия:

  1. Arena.ofConfined(): создает область памяти (арену), которая автоматически освобождается при закрытии блока try. Это предотвращает утечки памяти.

  2. arena.allocateFrom("..."): выделяет память для переданной строки Java и копирует эту строку в выделенную нативную память, добавляя в конец нулевой байт (\0), чтобы формат соответствовал стандарту C-строк. Возвращает объект MemorySegment - по сути, безопасный указатель для работы с сегментом памяти.

И далее собственно идет вызов гативной функции printf() и получение результата:

int result = (int) printf.invoke(str);

В итоге мы имеем следующий общий алгоритм работы программы:

  1. Поиск. Находит функцию printf в системных библиотеках.

  2. Выделение памяти. Резервирует блок памяти вне Java для строки.

  3. Выполнение. Вызывает нативную функцию, передавая ей адрес этого блока.

  4. Освобождение. После выполнения блока try память, выделенная под строку, мгновенно освобождается.

Стоит отметить, что как и в случае с JNI, для запуска программы необходимо передать утилите java дополнительную опцию, которая разрешает использование нативного кода в программе на Java. Если модули не используются, то программа запускается следующим образом:

java --enable-native-access=ALL-UNNAMED ...

В модульной же программе необходимо перечислить все модули, для которых разрешен нативный доступ:

java --enable-native-access=module1, module2 ...

Протестируем программу, запустив ее на выполнение:

eugene@Eugene:/workspace/java/test$ javac Program.java
eugene@Eugene:/workspace/java/test$ java --enable-native-access=ALL-UNNAMED Program
Hello METANIT.COM
Напечатано символов 18
eugene@Eugene:/workspace/java/test$ 
Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850