Для поиска библиотечных функций языка Си и их подготовке для вызова в API FFM применяется класс java.lang.foreign.Linker. Для создания линкера, связанного с текущей нативной платформой ОС применяется статический метод Linker.nativeLinker():
Linker linker = Linker.nativeLinker();
Фактически, часть "поиска" делегируется экземпляру SymbolLookup, который определяется с помощью метода defaultLookup() линкера:
Linker linker = Linker.nativeLinker(); SymbolLookup stdlib = linker.defaultLookup();
Библиотеки, о которых знает поиск по умолчанию, зависят от платформы. В Linux это libc и libm. Этот поиск достаточен для таких известных функций, как printf и malloc.
Для использования со специализированной библиотекой (или своей) в метод SymbolLookup.libraryLookup() передается путь к файлу библиотеки (в виде строки или объекта Path) и арена:
SymbolLookup somelib = SymbolLookup.libraryLookup("/lib/x86_64-linux-gnu/libsxxx.so", arena);
Метод libraryLookup загружает библиотеку. А когда арена закрывается, библиотека выгружается.
Получив объект SymbolLookup, мы можем перейти к поиску функций. Для этого SymbolLookup предоставляет два метода:
Optional<MemorySegment> find(String name): возвращает адрес символа с заданным именем
MemorySegment findOrThrow(String name): возвращает адрес символа с заданным именем или генерирует исключение, если функция не найдена
В обоих случаях указатель на полученную функцию возвращается через объект MemorySegment, работа с которым была рассмотрена в предыдущеихз статьях. Например, поиск функции printf:
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
MemorySegment printf_addr = stdlib.findOrThrow("printf");
Получив адрес вызываемой функции, можн создать дескриптор метода для ее вызова. Дескриптор метода - это объект, через который можно передавать аргументы, вызывать метод и получать возвращаемое значение.
Дескриптор метода представляет тип MethodHandle. Для его создания линкер - объект Linker предоставляет метода downcallHandle()
downcallHandle(FunctionDescriptor function, Linker.Option... options)
Создает дескриптор метода, используемый для вызова внешней функции с заданной сигнатурой
downcallHandle(MemorySegment address, FunctionDescriptor function, Linker.Option... options)
Создает дескриптор метода, используемый для вызова внешней функции с заданной сигнатурой и адресом
Этот метод принимает до трех параметров:
address: сегмент памяти, базовый адрес которого совпадает с адресом вызываемой внешней функции
function: дескриптор вызываемой внешней функции
options: параметры линкера, связанные с этим запросом на компоновку
Ключевой здесь параметр - это десткриптор функции, представленный интерфейсом FunctionDescriptor. Для определения этого объекта обычно применяется статический метод
FunctionDescriptor.of
FunctionDescriptor.of(MemoryLayout resLayout, MemoryLayout... argLayouts)
Его первый параметр представляет компоновку памяти типа результата функции, а второй параметр - компоновки памяти для параметров функции.
Например, получим десткриптор метода для функции printf:
import java.lang.foreign.*;
import java.lang.invoke.*;
public class Program {
public static void main(String[] args) {
// получаем линкер/компоновщик
Linker linker = Linker.nativeLinker();
// получаем стандартную библиотеку
SymbolLookup stdlib = linker.defaultLookup();
// получаем адрес функции printf
MemorySegment printf_addr = stdlib.findOrThrow("printf");
// дескриптор функции printf
FunctionDescriptor printf_desc = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS);
// дескриптор метода для функции printf
MethodHandle printf = linker.downcallHandle(printf_addr, printf_desc);
System.out.println(printf); // MethodHandle(MemorySegment)int
}
}
При передаче указателя в функцию C или получении результата указателя используется ValueLayout.ADDRESS. Тип блока памяти не имеет значения. Кроме того, необходимо точно указать тип
возвращаемого значения функции и типы параметров. Если предоставить неверную информацию, виртуальная машина может завершиться с ошибкой.
Так, функция printf в C возвращает целое число int, а в качестве параметра принимает строку (по сути указатель на символы), поэтому ее дескриптор имеет форму
FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS)
Получив дескриптор метода, наконец мы можем вызвать функцию. Для этого у типа MethodHandle применяется метод invoke()
public final Object invoke(Object... args) throws Throwable
В качестве аргументов в вызов метода передаются значения для параметров внешней функции. ПРи этом параметры должны соответствовать типам языка Си, а не Java. Например, мы не можем написать так:
printf.invoke("Hello");
Хотя, казалось бы функция printf() принимает строку. Однако это должна быть строка языка Си. Соответственно нам надо сконвертировать строку Java в строку Си (UTF-8).
Пример такого преобразования и вызова функции printf:
import java.lang.foreign.*;
import java.lang.invoke.*;
public class Program {
public static void main(String[] args) throws Throwable{
// получаем линкер/компоновщик
Linker linker = Linker.nativeLinker();
// получаем стандартную библиотеку
SymbolLookup stdlib = linker.defaultLookup();
// получаем адрес функции printf
MemorySegment printf_addr = stdlib.findOrThrow("printf");
// дескриптор функции printf
FunctionDescriptor printf_desc = FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS);
// дескриптор метода для функции printf
MethodHandle printf = linker.downcallHandle(printf_addr, printf_desc);
// создаем арену
try (Arena arena = Arena.ofConfined()) {
// строка для вывода
String str = "Hello METANIT.COM\n";
// преобразуем строку Java в строку UTF-8
MemorySegment str_seg = arena.allocateFrom(str);
// вызываем функцию printf и получаем результат
int char_count = (int) printf.invoke(str_seg); // Hello METANIT.COM
System.out.println("Длина строки: " + char_count + " символов"); // Длина строки: 18 символов
}
}
}
Отдельно стоит сказать про функции, который возвращают void. В этом случае для определения дескриптора функции применяется метод FunctionDescriptor.ofVoid
FunctionDescriptor ofVoid(MemoryLayout... argLayouts)
В качестве параметров методу также передаются значения, которые передаются вызываемой внешней функции. Указатель на void (void*), как и все указатели, представляют тип ValueLayout.ADDRESS.
Например, получим дескрипторы методов для функций malloc (выделение памяти) и free (освобождение памяти)
import java.lang.foreign.*;
import java.lang.invoke.*;
public class Program {
public static void main(String[] args) throws Throwable{
// получаем линкер/компоновщик
Linker linker = Linker.nativeLinker();
// получаем стандартную библиотеку
SymbolLookup stdlib = linker.defaultLookup();
// получаем адрес функции malloc
MemorySegment mallocAddr = stdlib.findOrThrow("malloc");
// void* malloc(size_t)
MethodHandle mallocHandle = linker.downcallHandle(mallocAddr, FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.JAVA_LONG));
System.out.println(mallocHandle); // MethodHandle(long)MemorySegment
// получаем адрес функции free
MemorySegment freeAddr = stdlib.findOrThrow("free");
// void free(void *ptr)
MethodHandle freeHandle = linker.downcallHandle(freeAddr, FunctionDescriptor.ofVoid(ValueLayout.ADDRESS));
System.out.println(freeHandle); // MethodHandle(MemorySegment)void
}
}