Рубрики
Без рубрики

Использование JNA для доступа к собственным динамическим библиотекам

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

Автор оригинала: Philippe Sevestre.

1. Обзор

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

2. Почему JNA?

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

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

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

  • Требуется, чтобы разработчики писали “клеевой код” на C/C++ для соединения Java и собственного кода
  • Требуется полный набор инструментов для компиляции и компоновки, доступный для каждой целевой системы
  • Маршалинг и удаление значений из JVM-это утомительная и подверженная ошибкам задача
  • Юридические и вспомогательные проблемы при смешивании Java и собственных библиотек

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

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

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

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

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

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


    net.java.dev.jna
    jna-platform
    5.6.0

Последнюю версию jna-платформы можно загрузить с Maven Central.

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

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

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

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

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

#include 
#include 
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);
}

Затем мы используем класс Native JNA для создания конкретной реализации этого интерфейса, чтобы мы могли вызвать наш 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, обычной практикой является объявление ПРИМЕР поле как часть объявления интерфейса:

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

4.2. Отображение основных типов

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

  • char => байт
  • короткий => короткий
  • char_t => char
  • int => int
  • long => com.sun.jna.NativeLong
  • долго долго => долго
  • float => поплавок
  • двойной => двойной
  • char * => Строка

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

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

4.3. Структуры и союзы

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

Например, учитывая эту структуру 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;
};

Теперь давайте использовать My Union с гипотетической библиотекой:

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

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

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

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

JNA предлагает абстракцию Pointer , которая помогает работать с 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);

Метод set Memory() просто заполняет базовый буфер постоянным байтовым значением (в данном случае нулем). Обратите внимание, что экземпляр 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  from Visual Studio
#define errno (*_errno())

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

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

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

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 system в true
  • Вызов Native.setProtected(true)

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

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 .