На дворе 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)
novoselov
29.09.2021 20:13Интересно как будет работать
MemorySegment.mapFile
иMemorySegment.ofByteBuffer
?Насколько я знаю
MappedByteBuffer
так и не починили, теперь появится альтернатива?
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();
Ну понятно, что все эти танцы с бубнами можно в свои методы один раз обернуть.
novoselov
30.09.2021 10:57Подход понятен, осталось сделать close и поддержку под Windows, etc :)
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 ради интереса. посмотреть какие результаты будут.
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. Но играться уже можно.
П.С. Спасибо автору за статью. Сам хотел написать, но автор был быстее
sandersru
30.09.2021 07:23Native image с GraalVM создать не удалось
Что именно не удалось? Откомпилять этот код в Грааль или написать под Грааль? Там сейчас разное API. Судя по коду, под грааль это вполне реализуемо, если интересно, пишите в личку, поделюсь опытом.
P.S. Я сейчас как раз подумываю по этой теме скрестить Vertx.fileSystem и AIO под JVM/GraalMode. В теории они должны прекрасно друг другу подойти.
sandersru
Очень интересная фишка и ждем давно. Правда для себя еще не решил, стоит ли "щупать", пока не вышло с инкубатора или нет.
Надеюсь они еще все же сделают, чтобы интерфейсы native в GraalVM и в Panama были одним и тем же.