Пакет jdk.incubator.foreign
Классы для поддержки доступа к внешней памяти/функциям на низком уровне и с высокой эффективностью, напрямую из Java.
Доступ к внешней памяти
Основные абстракции, введенные для поддержки доступа к внешней памяти, это MemorySegment и MemoryAddress. Первый моделирует непрерывный регион памяти, который может находиться как внутри, так и вне кучи Java; последний моделирует адрес — который также может находиться как внутри, так и вне кучи Java (и иногда может быть выражен как смещение в данном сегменте). Сегмент памяти представляет собой основную координату доступа к памяти для переменной var handle, которая может быть получена с помощью методов-комбинаторов, определенных в классе MemoryHandles; набор общих операций дереференции также предоставляется классом MemoryAccess, который может быть полезен для простого, неструктурированного доступа. Наконец, иерархия классов MemoryLayout позволяет описывать макеты памяти и выполнять базовые операции, такие как вычисление размера в байтах данного макета, получение его требований к выравниванию и т. д. Макеты памяти также предоставляют альтернативный, более абстрактный способ для создания переменных var handle для доступа к памяти, например, с использованием путей макета. Например, чтобы выделить внекучный регион памяти, достаточно большой для хранения 10 значений примитивного типа int, и заполнить его значениями в диапазоне от 0 до 9, мы можем использовать следующий код:
MemorySegment segment = MemorySegment.allocateNative(10 * 4, ResourceScope.newImplicitScope());
for (int i = 0 ; i < 10 ; i++) {
MemoryAccess.setIntAtIndex(segment, i, 42);
}
Здесь создается нативный сегмент памяти, то есть сегмент памяти, поддерживаемый внекучной памятью; размер сегмента составляет 40 байт, достаточно для хранения 10 значений примитивного типа int. Внутри цикла мы затем инициализируем содержимое сегмента памяти с помощью вспомогательного метода MemoryAccess.setIntAtIndex(jdk.incubator.foreign.MemorySegment, long, int); более конкретно, если мы рассматриваем сегмент памяти как набор из 10 смежных слотов, s[i], где 0 <= i < 10, где размер каждого слота составляет ровно 4 байта, логика инициализации выше установит каждый слот так, что s[i] = i, опять же, где 0 <= i < 10. Деаллокация с определенным порядком
При написании кода, который манипулирует сегментами памяти, особенно если они поддерживаются памятью, которая находится вне кучи Java, часто имеет решающее значение, чтобы ресурсы, связанные с сегментом памяти, освобождались, когда сегмент больше не используется, и своевременно. По этой причине в некоторых случаях ожидание, пока сборщик мусора определит, что сегмент недоступен, не является оптимальным. Клиенты, которые работают по этим предположениям, могут захотеть программно освободить память, связанную с сегментом памяти. Это можно сделать, используя абстракциюResourceScope, как показано ниже:
try (ResourceScope scope = ResourceScope.newConfinedScope()) {
MemorySegment segment = MemorySegment.allocateNative(10 * 4, scope);
for (int i = 0 ; i < 10 ; i++) {
MemoryAccess.setIntAtIndex(segment, i, 42);
}
}
Этот пример почти идентичен предыдущему; на этот раз мы сначала создаем так называемый объем ресурсов, который используется для связывания жизненного цикла созданного сразу после этого сегмента. Обратите внимание на использование конструкции try-with-resources: этот прием гарантирует, что все ресурсы памяти, связанные с сегментом, будут освобождены в конце блока в соответствии с семантикой, описанной в разделе 14.20.3 спецификации языка Java. Безопасность
Этот API обеспечивает строгие гарантии безопасности в отношении доступа к памяти. Во-первых, при дереференции сегмента памяти координаты доступа проверяются (при доступе), чтобы убедиться, что доступ не происходит по адресу, который находится вне границ сегмента памяти, используемого операцией дереференции. Мы называем эту гарантию пространственной безопасностью; другими словами, доступ к сегментам памяти проверяется на соответствие границам, так же как и доступ к массивам, как описано в разделе 15.10.4 спецификации языка Java.Поскольку сегменты памяти могут быть закрыты (см. выше), сегменты также проверяются (при доступе), чтобы убедиться, что область ресурсов, связанная с сегментом, к которому осуществляется доступ, не была закрыта преждевременно. Мы называем эту гарантию временной безопасностью. Вместе пространственная и временная безопасность гарантируют, что каждая операция доступа к памяти либо выполняется успешно — и обращается к допустимому расположению памяти — либо завершается неудачей.
Доступ к внешней функции
Основными абстракциями, введенными для поддержки доступа к внешним функциям, являютсяSymbolLookup и CLinker. Первый используется для поиска символов внутри нативных библиотек; последний предоставляет возможности связывания, которые позволяют моделировать внешние функции как экземпляры MethodHandle, так что клиенты могут выполнять вызовы внешних функций непосредственно в Java без необходимости промежуточных слоев нативного кода (как это происходит с Java Native Interface (JNI)). Например, для вычисления длины строки с использованием функции стандартной библиотеки C strlen на платформе Linux x64 мы можем использовать следующий код:
MethodHandle strlen = CLinker.getInstance().downcallHandle(
CLinker.systemLookup().lookup("strlen").get(),
MethodType.methodType(long.class, MemoryAddress.class),
FunctionDescriptor.of(CLinker.C_LONG, CLinker.C_POINTER)
);
try (var scope = ResourceScope.newConfinedScope()) {
var cString = CLinker.toCString("Hello", scope);
long len = (long)strlen.invokeExact(cString.address()); // 5
}
Здесь мы ищем символ strlen в поиске по системе. Затем мы получаем экземпляр линковщика (см. CLinker.getInstance()) и используем его для получения дескриптора метода, который указывает на символ библиотеки strlen. Для успешного завершения связывания необходимо предоставить (i) экземпляр MethodType, описывающий тип результирующего дескриптора метода, и (ii) экземпляр FunctionDescriptor, описывающий сигнатуру функции strlen. На основании этой информации линковщик однозначно определит последовательность шагов, которые превратят вызов дескриптора метода (здесь выполненный с помощью MethodHandle.invokeExact(java.lang.Object...)) в вызов внешней функции в соответствии с правилами платформенной ABI C. Класс CLinker также предоставляет множество полезных методов для взаимодействия с нативным кодом, таких как преобразование строк Java в нативные строки и наоборот (см. CLinker.toCString(java.lang.String, ResourceScope) и CLinker.toJavaString(jdk.incubator.foreign.MemorySegment) соответственно), как показано в приведенном выше примере. Внешние адреса
При создании сегмента памяти из кода Java свойства сегмента (пространственные границы, временные границы и ограничение) полностью известны при создании сегмента. Но при взаимодействии с нативными библиотеками клиенты часто получают сырые указатели; такие указатели не имеют пространственных границ (например, относится ли тип Cchar* к одному значению char, или массиву значений char, заданного размера?), понятия временных границ или ограничений потока. Когда клиенты получают экземпляр MemoryAddress из вызова внешней функции, может потребоваться получить экземпляр MemorySegment для дереференции памяти, на которую указывает этот адрес. Для этого клиенты могут действовать тремя различными способами, описанными ниже.
Во-первых, если известно, что адрес памяти принадлежит сегменту, которым клиент уже владеет, можно выполнить операцию перебазирования; другими словами, клиент может спросить у адреса, какое его смещение относительно данного сегмента, и затем продолжить дериференцию исходного сегмента соответственно, как показано ниже:
MemorySegment segment = MemorySegment.allocateNative(100, scope);
...
MemoryAddress addr = ... //obtain address from native code
int x = MemoryAccess.getIntAtOffset(segment, addr.segmentOffset(segment));
Во-вторых, если у клиента нет сегмента, содержащего данный адрес памяти, он может создать его неосторожно, используя фабрику MemoryAddress.asSegment(long, ResourceScope). Это позволяет клиенту внести дополнительную информацию о пространственных границах, которая, например, может быть доступна в документации внешней функции, которая сгенерировала нативный адрес. Вот как можно создать небезопасный сегмент из нативного адреса:
ResourceScope scope = ... // initialize a resource scope object
MemoryAddress addr = ... //obtain address from native code
MemorySegment segment = addr.asSegment(4, scope); // segment is 4 bytes long
int x = MemoryAccess.getInt(segment);
В-третьих, клиент может использовать так называемый сегмент все — то есть первобытный сегмент, который охватывает всю нативную кучу. Этот сегмент можно получить, вызвав метод MemorySegment.globalNativeSegment(), чтобы дериференция могла происходить без необходимости создания дополнительных экземпляров сегментов:
MemoryAddress addr = ... //obtain address from native code
int x = MemoryAccess.getIntAtOffset(MemorySegment.globalNativeSegment(), addr.toRawLongValue());
Обратные вызовы
ИнтерфейсCLinker также позволяет преобразовать существующий дескриптор метода (который может указывать на метод Java) в нативный адрес памяти (см. MemoryAddress), так что код Java может быть эффективно передан другим внешним функциям. Например, мы можем написать метод, сравнивающий два целочисленных значения, следующим образом:
class IntComparator {
static int intCompare(MemoryAddress addr1, MemoryAddress addr2) {
return MemoryAccess.getIntAtOffset(MemorySegment.globalNativeSegment(), addr1.toRawLongValue()) -
MemoryAccess.getIntAtOffset(MemorySegment.globalNativeSegment(), addr2.toRawLongValue());
}
}
Вышеупомянутый метод дериференцирует два адреса памяти, содержащие целое значение, и выполняет простое сравнение, возвращая разность этих значений. Затем мы можем получить дескриптор метода, который указывает на вышеуказанный статический метод, следующим образом:
MethodHandle intCompareHandle = MethodHandles.lookup().findStatic(IntComparator.class,
"intCompare",
MethodType.methodType(int.class, MemoryAddress.class, MemoryAddress.class));
Теперь, когда у нас есть экземпляр дескриптора метода, мы можем связать его с новым нативным адресом памяти, используя интерфейс CLinker, как показано ниже:
ResourceScope scope = ...
MemoryAddress comparFunc = CLinker.getInstance().upcallStub(
intCompareHandle,
FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER),
scope
);
Как и прежде, нам нужен экземпляр FunctionDescriptor, описывающий сигнатуру указателя функции, который мы хотим создать; как и прежде, это, вместе с типом дескриптора метода, однозначно определяет последовательность шагов, которые позволят коду внешнего приложения вызвать intCompareHandle в соответствии с правилами платформенной ABI C. Жизненный цикл адреса памяти, возвращаемого методом CLinker.upcallStub(java.lang.invoke.MethodHandle, jdk.incubator.foreign.FunctionDescriptor, jdk.incubator.foreign.ResourceScope), связан с параметром объема ресурсов, переданным в этот метод. Ограниченные методы
Некоторые методы в этом пакете считаются ограниченнымиMemoryAddress.asSegment(long, ResourceScope) может использоваться для создания нового сегмента с заданными пространственными границами из исходного адреса. Привязка внешних данных и/или функций обычно небезопасна и, если выполнена неправильно, может привести к сбоям VM или к повреждению памяти при обращении к привязанному элементу Java API. Например, в случае MemoryAddress.asSegment(long, ResourceScope), если предоставленные пространственные границы некорректны, клиент возвращаемого этим методом сегмента может вызвать сбой VM или повредить память при попытке разыменования указанного сегмента. По этим причинам крайне важно, чтобы код, вызывающий ограниченный метод, никогда не передавал аргументы, которые могут привести к некорректной привязке внешних данных и/или функций к Java API.
Доступ к ограниченным методам по умолчанию отключен; чтобы включить ограниченные методы, в командной строке параметр --enable-native-access должен содержать имя модуля вызывающего приложения.
| Класс | Описание |
|---|---|
| Addressable | Представляет тип, который является адресуемым. |
| CLinker | C-линкер реализует соглашения о вызовах C Application Binary Interface (ABI). |
| CLinker.TypeKind | Тип C. |
| CLinker.VaList | Интерфейс, который моделирует C va_list. |
| CLinker.VaList.Builder | Интерфейс-строитель, используемый для построения C va_list. |
| FunctionDescriptor | Дескриптор функции состоит из нуля или более макетов аргументов и нуля или одного макета возвращаемого значения. |
| GroupLayout | Макет группы используется для объединения нескольких макетов членов. |
| MemoryAccess | Этот класс определяет готовые статические методы доступа, которые могут использоваться для разыменования сегментов памяти различными способами. |
| MemoryAddress | Адрес памяти моделирует ссылку на расположение в памяти. |
| MemoryHandles | Этот класс определяет несколько фабричных методов для создания и объединения дескрипторов доступа к памяти. |
| MemoryLayout | Макет памяти может использоваться для описания содержимого сегмента памяти в нейтральном к языку формате. |
| MemoryLayout.PathElement | Экземпляры этого класса используются для формирования путей макета. |
| MemoryLayouts | Этот класс определяет полезные константы макета. |
| MemorySegment | Сегмент памяти моделирует непрерывный регион памяти. |
| ResourceScope | Область ресурсов управляет жизненным циклом одного или нескольких ресурсов. |
| ResourceScope.Handle | Абстракция, моделирующая обработчик области ресурсов. |
| SegmentAllocator | Этот интерфейс моделирует аллокатор памяти. |
| SequenceLayout | Макет последовательности. |
| SymbolLookup | Поиск символов. |
| ValueLayout | Макет значения. |
© 1993, 2021, Oracle and/or its affiliates. All rights reserved.
Documentation extracted from Debian's OpenJDK Development Kit package.
Licensed under the GNU General Public License, version 2, with the Classpath Exception.
Various third party code in OpenJDK is licensed under different licenses (see Debian package).
Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.
https://docs.oracle.com/en/java/javase/17/docs/api/jdk.incubator.foreign/jdk/incubator/foreign/package-summary.html