«1. Обзор

В этом руководстве мы увидим, как использовать библиотеку Java Native Access (сокращенно JNA) для доступа к собственным библиотекам без написания кода JNI (Java Native Interface).

2. Почему ЮНА?

В течение многих лет Java и другие языки на основе JVM в значительной степени соответствовали своему девизу «написал один раз, работает везде». Однако иногда нам нужно использовать собственный код для реализации некоторых функций:

    Повторное использование устаревшего кода, написанного на C/C++ или любом другом языке, способном создавать собственный код. Доступ к специфичным для системы функциям, недоступным в стандартной среде выполнения Java. Оптимизация скорости и/ или использование памяти для определенных разделов данного приложения.

Изначально такое требование означало, что нам придется прибегнуть к JNI — Java Native Interface. Несмотря на свою эффективность, этот подход имеет свои недостатки, и его, как правило, избегают из-за нескольких проблем:

    Требуется, чтобы разработчики написали «связующий код» C/C++ для соединения Java и собственного кода. Требуется полная цепочка инструментов для компиляции и компоновки, доступная для каждой цели. система Маршалинг и демаршалинг значений в JVM и из него — утомительная и подверженная ошибкам задача Юридические вопросы и проблемы поддержки при смешивании Java и собственных библиотек

JNA решила большую часть сложностей, связанных с использованием JNI. В частности, нет необходимости создавать какой-либо код JNI для использования собственного кода, расположенного в динамических библиотеках, что значительно упрощает весь процесс.

Конечно, есть некоторые компромиссы:

    Мы не можем напрямую использовать статические библиотеки Медленнее по сравнению с написанным вручную кодом JNI

Однако для большинства приложений преимущества простоты JNA намного перевешивают эти недостатки. Таким образом, будет справедливо сказать, что, если у нас нет очень специфических требований, JNA сегодня, вероятно, является лучшим доступным выбором для доступа к собственному коду из Java — или, кстати, любого другого языка на основе JVM.

3. Настройка проекта JNA

Первое, что нам нужно сделать, чтобы использовать JNA, это добавить ее зависимости в pom.xml нашего проекта:

<dependency>
    <groupId>net.java.dev.jna</groupId>
    <artifactId>jna-platform</artifactId>
    <version>5.6.0</version>
</dependency>

Последнюю версию jna-platform можно загрузить с Центральный Мейвен.

4. Использование JNA

Использование JNA представляет собой двухэтапный процесс:

    Сначала мы создаем интерфейс Java, который расширяет интерфейс библиотеки JNA для описания методов и типов, используемых при вызове целевого машинного кода. Затем мы передать этот интерфейс в JNA, который возвращает конкретную реализацию этого интерфейса, которую мы используем для вызова собственных методов

4.1. Вызов методов из стандартной библиотеки C

В нашем первом примере давайте воспользуемся JNA для вызова функции cosh из стандартной библиотеки C, доступной в большинстве систем. Этот метод принимает двойной аргумент и вычисляет его гиперболический косинус. Программа AC может использовать эту функцию, просто включив заголовочный файл \u003cmath.h\u003e:

#include <math.h>
#include <stdio.h>
int main(int argc, char** argv) {
    double v = cosh(0.0);
    printf("Result: %f\n", v);
}

Давайте создадим интерфейс Java, необходимый для вызова этого метода:

public interface CMath extends Library { 
    double cosh(double value);
}

Затем мы используем класс JNA Native для создания конкретная реализация этого интерфейса, чтобы мы могли вызвать наш API:

CMath lib = Native.load(Platform.isWindows()?"msvcrt":"c", CMath.class);
double result = lib.cosh(0);

Действительно интересная часть здесь — это вызов метода load(). Он принимает два аргумента: имя динамической библиотеки и интерфейс Java, описывающий методы, которые мы будем использовать. Он возвращает конкретную реализацию этого интерфейса, позволяя нам вызывать любой из его методов.

Теперь имена динамических библиотек обычно зависят от системы, и стандартная библиотека C не является исключением: libc.so в большинстве систем на базе Linux и msvcrt.dll в Windows. Вот почему мы использовали вспомогательный класс Platform, включенный в JNA, чтобы проверить, на какой платформе мы работаем, и выбрать правильное имя библиотеки.

Обратите внимание, что нам не нужно добавлять расширения .so или .dll, поскольку они подразумеваются. Кроме того, для систем на базе Linux нам не нужно указывать префикс «lib», который является стандартным для разделяемых библиотек.

Поскольку динамические библиотеки ведут себя как синглтоны с точки зрения Java, обычной практикой является объявление поля INSTANCE как части объявления интерфейса:

public interface CMath extends Library {
    CMath INSTANCE = Native.load(Platform.isWindows() ? "msvcrt" : "c", CMath.class);
    double cosh(double value);
}

4.2. Сопоставление основных типов

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

    char =\u003e byte short =\u003e short wchar_t =\u003e char int =\u003e int long =\u003e com.sun.jna.NativeLong long long = \u003e long float =\u003e float double =\u003e double char * =\u003e String

Отображение, которое может показаться странным, используется для собственного типа long. Это связано с тем, что в C/C++ тип long может представлять 32- или 64-разрядное значение, в зависимости от того, используем ли мы 32- или 64-разрядную систему.

Чтобы решить эту проблему, JNA предоставляет тип NativeLong, который использует правильный тип в зависимости от архитектуры системы.

4.3. Структуры и объединения

Еще один распространенный сценарий — работа с API-интерфейсами собственного кода, которые ожидают указатель на некоторую структуру или тип объединения. При создании интерфейса Java для доступа к нему соответствующий аргумент или возвращаемое значение должны быть типом Java, расширяющим Structure или Union соответственно.

Например, для этой структуры C:

struct foo_t {
    int field1;
    int field2;
    char *field3;
};

Его одноранговый класс Java будет:

@FieldOrder({"field1","field2","field3"})
public class FooType extends Structure {
    int field1;
    int field2;
    String field3;
};

JNA требует аннотации @FieldOrder, чтобы он мог правильно сериализовать данные в буфер памяти, прежде чем использовать их как аргумент целевого метода.

Кроме того, мы можем переопределить метод getFieldOrder() для того же эффекта. При нацеливании на одну архитектуру/платформу первый метод, как правило, достаточно хорош. Мы можем использовать последний для решения проблем выравнивания на разных платформах, которые иногда требуют добавления некоторых дополнительных полей заполнения.

Объединения работают аналогично, за исключением нескольких моментов:

    Нет необходимости использовать аннотацию @FieldOrder или реализовывать getFieldOrder() Мы должны вызвать setType() перед вызовом нативного метода

Давайте посмотрим, как это сделать на простом примере:

public class MyUnion extends Union {
    public String foo;
    public double bar;
};

Теперь давайте воспользуемся MyUnion с гипотетической библиотекой:

MyUnion u = new MyUnion();
u.foo = "test";
u.setType(String.class);
lib.some_method(u);

Если и foo, и bar, где одного и того же типа, вместо этого нам придется использовать имя поля: ~ ~~

u.foo = "test";
u.setType("foo");
lib.some_method(u);

4.4. Использование указателей

JNA предлагает абстракцию указателя, которая помогает работать с API, объявленными с нетипизированным указателем — обычно это void *. Этот класс предлагает методы, которые позволяют читать и записывать доступ к базовому собственному буферу памяти, что имеет очевидные риски.

Прежде чем начать использовать этот класс, мы должны быть уверены, что четко понимаем, кто «владеет» упомянутой памятью в каждый момент времени. В противном случае, скорее всего, возникнут трудно отлаживаемые ошибки, связанные с утечками памяти и/или недопустимым доступом.

Предполагая, что мы знаем, что делаем (как всегда), давайте посмотрим, как мы можем использовать хорошо известные функции malloc() и free() с JNA, используемые для выделения и освобождения буфера памяти. Во-первых, давайте снова создадим наш интерфейс-оболочку:

public interface StdC extends Library {
    StdC INSTANCE = // ... instance creation omitted
    Pointer malloc(long n);
    void free(Pointer p);
}

Теперь давайте воспользуемся им для выделения буфера и поиграем с ним:

StdC lib = StdC.INSTANCE;
Pointer p = lib.malloc(1024);
p.setMemory(0l, 1024l, (byte) 0);
lib.free(p);

Метод setMemory() просто заполняет базовый буфер постоянным значением байта. (ноль в данном случае). Обратите внимание, что экземпляр Pointer понятия не имеет, на что он указывает, не говоря уже о его размере. Это означает, что мы можем довольно легко испортить нашу кучу, используя ее методы.

Позже мы увидим, как мы можем смягчить такие ошибки, используя функцию защиты от сбоев JNA.

4.5. Обработка ошибок

Старые версии стандартной библиотеки C использовали глобальную переменную errno для хранения причины сбоя конкретного вызова. Например, вот как типичный вызов open() будет использовать эту глобальную переменную в C:

int fd = open("some path", O_RDONLY);
if (fd < 0) {
    printf("Open failed: errno=%d\n", errno);
    exit(1);
}

Конечно, в современных многопоточных программах этот код не будет работать, верно? Что ж, благодаря препроцессору C разработчики все еще могут писать такой код, и он будет отлично работать. Оказывается, в настоящее время errno — это макрос, который расширяется до вызова функции:

// ... excerpt from bits/errno.h on Linux
#define errno (*__errno_location ())

// ... excerpt from <errno.h> from Visual Studio
#define errno (*_errno())

Теперь этот подход отлично работает при компиляции исходного кода, но при использовании JNA такого не происходит. Мы могли бы объявить расширенную функцию в нашем интерфейсе оболочки и вызвать ее явно, но JNA предлагает лучшую альтернативу: LastErrorException.

«Любой метод, объявленный в интерфейсах-оболочках с бросками LastErrorException, будет автоматически включать проверку на наличие ошибки после нативного вызова. Если он сообщает об ошибке, JNA выдает исключение LastErrorException, которое включает исходный код ошибки.

Давайте добавим несколько методов в интерфейс оболочки StdC, который мы использовали ранее, чтобы показать эту функцию в действии:

public interface StdC extends Library {
    // ... other methods omitted
    int open(String path, int flags) throws LastErrorException;
    int close(int fd) throws LastErrorException;
}

Теперь мы можем использовать open() в предложении try/catch:

StdC lib = StdC.INSTANCE;
int fd = 0;
try {
    fd = lib.open("/some/path",0);
    // ... use fd
}
catch (LastErrorException err) {
    // ... error handling
}
finally {
    if (fd > 0) {
       lib.close(fd);
    }
}

В блоке catch мы можем использовать LastErrorException.getErrorCode(), чтобы получить исходное значение errno и использовать его как часть логики обработки ошибок.

4.6. Обработка нарушений доступа

Как упоминалось ранее, JNA не защищает нас от неправильного использования данного API, особенно при работе с буферами памяти, передаваемыми туда и обратно нативному коду. В обычных ситуациях такие ошибки приводят к нарушению прав доступа и завершают работу JVM.

JNA в некоторой степени поддерживает метод, который позволяет коду Java обрабатывать ошибки нарушения прав доступа. Есть два способа активировать его:

    Установка системного свойства jna.protected в true Вызов Native.setProtected(true)

Как только мы активируем этот защищенный режим, JNA будет перехватывать ошибки нарушения доступа, которые обычно приводят к сбой и исключение java.lang.Error. Мы можем убедиться, что это работает, используя Pointer, инициализированный с недопустимым адресом, и пытаясь записать в него некоторые данные:

Native.setProtected(true);
Pointer p = new Pointer(0l);
try {
    p.setMemory(0, 100*1024, (byte) 0);
}
catch (Error err) {
    // ... error handling omitted
}

Однако, как указано в документации, эту функцию следует использовать только в целях отладки/разработки.

5. Заключение

В этой статье мы показали, как использовать JNA для более легкого доступа к собственному коду по сравнению с JNI.

Как обычно, весь код доступен на GitHub.