На дворе Java 17, а значит пора разобрать еще один интересный JEP, а именно JEP 412: Foreign Function & Memory API, который является переосмыслением двух предыдущих: Foreign-Memory Access API и Foreign Linker API.

История тянется еще с JDK 14 и по прежнему находится в инкубаторе, поэтому не забываем добавить -add-modules jdk.incubator.foreign для запуска.

Новый API дает возможность взаимодействовать с кодом и данными вне Java runtime. Теперь JVM может эффективно работать с нативными библиотеками и внешней памятью. Альтернатива JNI имеет улучшенную производительность и стабильность, а также возможность работать с разными видами памяти на разных платформах.

Function & Memory API предоставляют классы и интерфейсы для:

  • выделения внешней памяти MemorySegment, MemoryAddress, SegmentAllocator

  • управления памятью и доступа к ней MemoryLayout, MemoryHandles, MemoryAccess

  • управления жизненным циклом ресурсов ResourceScope

  • вызова внешних функций SymbolLookup, CLinker

Memory Segments

Memory Segments это абстракции, которые представляют участки памяти. Они связаны как пространственными, так и временными ограничениями. Пространственные ограничения гарантируют что действия с сегментом не затронут память за его границами, а временные - что операции не смогут происходить после закрытия области ресурсов.

Сегменты могут быть нескольких типов:

  • Native segments - выделены с нуля в нативной памяти

  • Mapped segments - врапперы смапленой памяти

  • Array or buffer segments - врапперы существующих java массивов или буфферов

MemorySegment nativeSegment = MemorySegment
									.allocateNative(100, ResourceScope.newImplicitScope());

MemorySegment mappedSegment = MemorySegment
                                        .mapFile( Path.of("memory.file"), 
                                                   0,
                                                   200,
                                                   READ_WRITE, 
                                                   newImplicitScope() );

MemorySegment arraySegment = MemorySegment.ofArray(new int[100]);

MemorySegment bufferSegment = MemorySegment.ofByteBuffer(ByteBuffer.allocateDirect(100));

Memory Layouts

MemoryLayout используются для декларативного описания сегментов памяти и дают возможность определить разбивку на элементы.

  • value layouts - описывают разметку со значениями базовых типов, таких как целочисленные и с плавающей точкой

  • padding layouts - используют в основном для выравнивания и представляют участки памяти, которые стоит игнорировать.

На примере ниже, sequence memory layout, который создает поочередно повторяющийся 32-битный layout 25 раз.

SequenceLayout intArrayLayout = MemoryLayout.sequenceLayout(25,                                              
                   MemoryLayout.valueLayout(32, ByteOrder.nativeOrder()));

Resource scopes

За жизненный цикл ресурсов отвечают resource scopes, которые бывают explicit (явные) и implicit (неявные):

  • explicit resource scopes, такие как, newConfinedScope() и newSharedScope() поддерживают детерминированное высвобождение ресурсов и могут быть явно закрыты методом close()

  • implicit resource scopes, например, newImplicitScope() не могут быть закрыты явно и вызов close() повлечет за собой эксепшн. Ресурсы освобождаются только после того как инстанс скоупа станет недоступен.

try (ResourceScope scope = ResourceScope.newConfinedScope()) {
      var mappedSegment = MemorySegment.mapFile(Path.of("my.file"), 0, 100000, FileChannel.MapMode.READ_WRITE, scope);
  
      var nativeSegment = MemorySegment.allocateNative(100, scope);
}

Скоупы ресурсов также можно разделить на: thread-confined, которые поддерживают строгое замыкание на поток и shared, с которыми могут взаимодействовать несколько потоков.

Segment allocators

Аллокаторы предоставляют методы для выделения и инициализации участков памяти. Интерфейс SegmentAllocator содержит фабричные методы для создания часто используемых аллокаторов и удобные методы для создания сегментов из примитивов и массивов.

Создадим аллокатор, выделим память и инициализируем массивом:

try (ResourceScope scope = ResourceScope.newConfinedScope()) {
    SegmentAllocator allocator = SegmentAllocator.arenaAllocator(scope);
    for (int i = 0 ; i < 100 ; i++) {
 					MemorySegment s = allocator
   											.allocateArray(C_INT, new int[] { 1, 2, 3 });
        ...
    }
    ...
}

Looking up foreign functions

Вызов внешних функций невозможен без загрузки нативных библиотек. В JNI эта цель достигалась при помощи System::loadLibrary и System::load методов. Библиотеки загруженные таким способом всегда связаны с класслоадером.

В FFM API механизм загрузки остался прежним. Но новый API позволяет находить адрес идентификаторов в загруженной библиотеке при помощи SymbolLookup, вызвать который можно двумя путями:

  • SymbolLookup::loaderLookup возвращает реализацию, которая ищет идентификаторы во всех библиотеках, загруженных текущим класслоадером

  • CLinker::systemLookup возвращает platform-dependent реализацию, которая ищет идентификаторы в стандартной C библиотеке

SymbolLookup::lookup(String) находит по имени метод в нативной библиотеке и возвращает MemoryAddress, который указывает на точку входа функции.

Для примера загрузим OpenGL библиотеку и найдем glGetString метод:

System.loadLibrary("GL");
SymbolLookup loaderLookup  = SymbolLookup.loaderLookup();
MemoryAddress clangVersion = loaderLookup.lookup("glGetString").get();

Linking Java code to foreign functions

Интерфейс CLinker описывает взаимодействие Java с нативным кодом. Основной фокус на отношениях Java и С, но концепт интерфейса подходит для поддержки других языков в будущем. Абстракция поддерживает downcalls и upcalls.

  • downcalls - вызывают нативные функции из Java кода как простой MethodHandle. Принимают в параметры MemoryAddress, полученный через lookup, MethodType, который описывает сигнатуру клиента и FunctionDescriptor по сути описывающий сигнатуру внешней функции.

  • upcalls - позволяют конвертировать существующий MethodHandle (который может ссылаться на обычный Java метод) в MemorySegment который может будет передан в нативную функцию как указатель.

Для примера вызовем функцию size_t strlen(const char *s); из С библиотеки:

MethodHandle strlen = CLinker.getInstance().downcallHandle(
    CLinker.systemLookup().lookup("strlen").get(),
    MethodType.methodType(long.class, MemoryAddress.class),
    FunctionDescriptor.of(C_LONG, C_POINTER)
);

MemorySegment str = CLinker.toCString("Hello", newImplicitScope());
long len = strlen.invokeExact(str.address());  // 5

Summary

Новый API для работы с нативными библиотеками это серьезных шаг в сторону безопасности таких операций. Много сценариев, которые требовали использование JNI, теперь могут решены при помощи FFM API.

В статье сделан краткий обзор ключевых моментов модуля jdk.incubator.foreign, основанный на JEP 412 и Javadocs 17-й версии. Думаю стоит присмотреться к новым возможностям.

Ждем выхода Foreign Function & Memory API из инкубатора.

Комментарии (9)


  1. sandersru
    29.09.2021 14:27
    +2

    Очень интересная фишка и ждем давно. Правда для себя еще не решил, стоит ли "щупать", пока не вышло с инкубатора или нет.

    Надеюсь они еще все же сделают, чтобы интерфейсы native в GraalVM и в Panama были одним и тем же.


  1. novoselov
    29.09.2021 20:13

    Интересно как будет работать MemorySegment.mapFile и MemorySegment.ofByteBuffer?

    Насколько я знаю MappedByteBufferтак и не починили, теперь появится альтернатива?


  1. sandersru
    29.09.2021 21:22

    Как будет работать не знаю, надо пробовать, но альтернатива багу есть:

    По опыту Graal Native и Java Native Access - просто идем в POSIX, дергаем open/read метод, куда передаем указатель и получаем результат.

    Судя по API что-то примерно такое (код не проверял, смотрел по спекам):

    MethodHandle open = CLinker.getInstance().downcallHandle(
            LibraryLookup.ofDefault().lookup("open").get(),
            MethodType.methodType(MemoryAddress.class, int.class);
    
    int fd = open.invoke(CLinker.toCString("/tmp/file.txt").address(), 0);
    
    MemorySegment ms = MemorySegment.allocateNative(1000);
    MethodHandle read = CLinker.getInstance().downcallHandle(
            LibraryLookup.ofDefault().lookup("read").get(),
            MethodType.methodType(int.class, MemoryAddress.class, int.class);
    read.invoke(fd, ms.address(), 1000);
    ByteBuffer buf = ms.asByteBuffer();

    Ну понятно, что все эти танцы с бубнами можно в свои методы один раз обернуть.


    1. novoselov
      30.09.2021 10:57

      Подход понятен, осталось сделать close и поддержку под Windows, etc :)


      1. sandersru
        30.09.2021 14:12

        Ну типа того... :)

        Хотя вот я тут ради интереса померял запись/чтение в random место в файл на 1GB блоков в 64kb, пока результат заставляет задуматься(либо я не то мерял)...

        java.io randomRead    thrpt         31135.552          ops/s
        java.nio randomRead   thrpt        993593.599          ops/s
        JNA randomRead        thrpt         28888.787          ops/s
        
        java.io randomWrite   thrpt         17482.929          ops/s
        java.nio randomWrite  thrpt       1039483.908          ops/s
        JNA randomWrite       thrpt         16280.924          ops/s

        На выходных попробую Linix AIO подергать в JNA и native ради интереса. посмотреть какие результаты будут.


  1. tmk826
    30.09.2021 01:51
    -1

    Ну в принципе что-то подобное я попробовал https://github.com/kofemann/vfs4j/blob/master/src/main/java/org/dcache/vfs4j/LocalVFS.java.

    В 17 работает хорошо. В 16 иногда ВМ падала. Native image с GraalVM создать не удалось. У продакшен пока рановато. В 18 ожидаются изменения в API. Но играться уже можно.

    П.С. Спасибо автору за статью. Сам хотел написать, но автор был быстее


  1. sandersru
    30.09.2021 07:23

    Native image с GraalVM создать не удалось

    Что именно не удалось? Откомпилять этот код в Грааль или написать под Грааль? Там сейчас разное API. Судя по коду, под грааль это вполне реализуемо, если интересно, пишите в личку, поделюсь опытом.

    P.S. Я сейчас как раз подумываю по этой теме скрестить Vertx.fileSystem и AIO под JVM/GraalMode. В теории они должны прекрасно друг другу подойти.


    1. tmk826
      30.09.2021 12:18

      Я так понимаю, что Грааль не видит API в инкубаторе и падает с UnresolvedElementException


      1. sandersru
        30.09.2021 14:18

        Я не собирал инкубатор под Грааль.

        Но достаточно посмотреть на JavaDoc из инкубатора и из Грааль тут и тут, чтобы понять, что API пошли разными путями.