null

Между C и Java

Доброе утро!

В прошлой статье я рассказывал, как в одном из проектов мы реализовали блокчейн. Чтобы вспомнить (или узнать), стоит сказать, что это была библиотека на C99, используемая порталом, написанном на Java. Сегодня я хочу рассказать, как мы их скрестили.

Для начала стоит провести краткий обзор (известных мне) технологий, позволяющих делать вызовы сишных функций из джавы. Я остановил свой взгляд на трёх: JNR, JNI и JNA.

JNR (Java Native Runtime) мне очень понравилась с точки зрения предоставляемого интерфейса. Уже есть обвязки над libc, аннотации для типов (в том числе, отсутствующих в Java беззнаковых) и так далее. В общем, впечатления очень приятные, для какого-нибудь личного проекта я бы такое может быть и использовал, но в нашем случае меня отпугнула её малая известность и не очень высокие темпы развития. В частности, я не понял, будет ли данный проект жить.

Следующим вариантом был классический JNI (Java Native Interface). Это наиболее производительный вариант и наиболее надёжный, так как вряд ли он куда-то пропадёт. Но так как у нас всего-лишь библиотека, предоставляющая наружу достаточно простой API, нам не было нужды городить кучу промежуточного плюсокода и я решил продолжить искать дальше.

И вот я нашёл JNA (Java Native Access). Об этой библиотеке, видимо, придётся рассказать поподробнее, так как именно её я выбрал в для упомянутого проекта.

 

Общая механика довольно проста: мы пишем какой-то код на C и компилируем его в виде динамической библиотеки. Далее мы создаём интерфейс, описывающий сигнатуры методов в библиотеке, описываем структуры в виде классов, если нам их надо принимать или передавать. Ну и достаточно, вроде как. Теперь JNA может использовать данный интерфейс, чтобы с libffi вызывать нужные нам методы.

 

Немножко примеров для ясности.

Положим, есть некая структура:

struct s {
  int a;
  long b;
};

А есть набор функций, с нею работающих:

int f_one(struct a*);
void f_two(struct a*, int x);

Реализация этих функций нам не важна, это совершенно обычные функции, которые никак не адаптировались под использование с Java. Всё это находится в некоторой libafuncs.so.

По упомянутой схеме напишем интерфейс:

public interface An extends Library {
  An INSTANCE = (An) Native.loadLibrary("afuncs", An.class);

  int f_one(ANative a);
  void f_two(ANative a, int x);

}

Что такое ANative? Это класс, структурно соответствующий типу A:

public interface ANative extends Structure {

  int a;
  long b;

  @Override
  protected List<String> getFieldOrder() {
    return Arrays.asList("a", "b");
  }

}

Да, здесь добавился некий метод getFieldOrder. Дело в том, что компилятор C оставил в структуре поля ровно в таком порядке, в каком они шли, разве что раздвинул их из-за выравнивания. JNA же выбирает поля из структуры с помощью рефлекции, и гарантий того, что получит она их в описанном в коде порядке нет. А отобразить на структуру их надо. Именно для этого предназначена эта функция.

В итоге у нас получилась обёртка над библиотекой, всё что нужно теперь ­— сделать класс, у которого, будет создаваться объект, соответствующий такому интерфейсу (static private final An an = An.INSTANCE, к примеру), методы будут именованы так, как это принято в джаве (а то коллеги-джависты почему-то неодобрительно смотрят на snake case), коды ошибок переведены в исключения, а пользователи данного API, по возможности, избавлены от необходимости работать с классами, отображающими сишные структуры (если этого явно не требуется).

 

Вот так и была написана библиотека, адаптирующая блокчейн на C к порталу на Java.