Про это и хочу кратко рассказать.
О проекте Panama на Хабре уже писали. Чтобы понять, что это и зачем, стоит прочитать интервью по ссылке. Я же просто покажу пару простых примеров того, как можно применить native binder.
Прежде всего, вам понадобится компилятор C. Если вы используете Linux или MacOS, то он у вас уже есть. Если Windows, то придётся сначала установить Build Tools for Visual Studio 2017. И, конечно же, вам потребуется OpenJDK с поддержкой «Панамы». Получить его можно либо сборкой ветки «foreign» соответствующего репозитория, либо загрузкой Early-Access билда.
Начнём с минимального примера — простой функции, складывающей два числа:
adder.h
#ifndef _ADDER_H
#define _ADDER_H
__declspec(dllexport) int add(int, int);
#endif
adder.c
#include "adder.h"
__declspec(dllexport)
int add(int a, int b) {
return a + b;
}
Компилируем в DLL
cl /LD adder.c
И используем в java-коде
import java.foreign.Library;
import java.foreign.Libraries;
import java.foreign.annotations.NativeHeader;
import java.foreign.annotations.NativeFunction;
import java.lang.invoke.MethodHandles;
public class App {
@NativeHeader
interface Adder {
@NativeFunction("(i32 i32)i32")
int add(int a, int b);
}
public static void main(String[] args) {
Library lib = Libraries.loadLibrary(MethodHandles.lookup(), "adder");
Adder adder = Libraries.bind(Adder.class, lib);
System.out.println(adder.add(3, 5));
}
}
В исходнике должно быть много знакомого для тех, кто использовал JNR: объявляется интерфейс нативной библиотеки, библиотека загружается, связывается с интерфейсом и происходит вызов нативной функции. Основное отличие — использование Layout Definition Language в аннотациях для описания схемы отображения нативных данных на типы Java.
Несложно догадаться, что выражение "
(i32 i32)i32
" обозначает функцию принимающую два целых 32-битных числа и возвращающую целое 32-битное число. Метка i
обозначает один из трёх основных типов — целое число с порядком байт little-endian. Кроме него часто встречаются u
и f
— беззнаковое целое и число с плавающей точкой, соответственно. Для обозначения порядка big-endian используются те же метки, но в верхнем регистре — I
, U
, F
. Ещё одна часто встречающаяся метка — это v
, используемая для void
. Число идущее следом за меткой обозначают количество используемых бит.Если число стоит перед меткой, то метка обозначает массив:
[42f32]
— массив из 42 элементов типа float
. Квадратные скобки группируют метки. Кроме массивов это может использоваться для обозначения структур ([i32 i32]
— структура с двумя полями типа int
) и объединений ([u64|u64:f32]
— long
или указатель на float
).Для обозначения указателей используется двоеточие. Например,
u64:i32
— указатель на int
, u64:v
— указатель типа void
, а u64:[i32 i32]
— указатель на структуру.Вооружившись этой информацией, немного усложним пример.
totalizer.c
__declspec(dllexport)
long sum(int vals[], int size) {
long r = 0;
for (int i = 0; i < size; i++) {
r += vals[i];
}
return r;
}
App.java
import java.foreign.Library;
import java.foreign.Libraries;
import java.foreign.NativeTypes;
import java.foreign.Scope;
import java.foreign.annotations.NativeHeader;
import java.foreign.annotations.NativeFunction;
import java.foreign.memory.Array;
import java.foreign.memory.Pointer;
import java.lang.invoke.MethodHandles;
public class App {
@NativeHeader
interface Totalizer {
@NativeFunction("(u64:i32 i32)u64")
long sum(Pointer<Integer> vals, int size);
}
public static void main(String[] args) {
Library lib = Libraries.loadLibrary(MethodHandles.lookup(),
"totalizer");
Totalizer totalizer = Libraries.bind(Totalizer.class, lib);
try (Scope scope = Scope.newNativeScope()) {
Array<Integer> array = scope.allocateArray(NativeTypes.INT,
new int[] { 23, 15, 4, 16, 42, 8 });
System.out.println(totalizer.sum(array.elementPointer(), 3));
}
}
}
В java-коде появилось сразу несколько новых элементов —
Scope
, Array
и Pointer
. Работая с нативным кодом, вам придётся оперировать off-heap данными, а значит придётся самостоятельно выделять память, самостоятельно освобождать и следить за актуальностью указателей. К счастью, есть Scope
, берущий на себя все эти заботы. Его методы позволяют легко и удобно выделять неспецифицированную память, память под массивы, структуры и C-строки, получать указатели на эту память, а так же автоматически освобождать её после завершения блока try-with-resources и менять состояние созданных указателей так, чтобы обращение к ним приводило к исключению, а не падению виртуальной машины.Чтобы посмотреть в работе структуры и указатели, усложним пример ещё немного.
mover.h
#ifndef _ADDER_H
#define _ADDER_H
typedef struct {
int x;
int y;
} Point;
__declspec(dllexport) void move(Point*);
#endif
mover.c
#include "mover.h"
__declspec(dllexport)
void move(Point *point) {
point->x = 4;
point->y = 2;
}
App.java
import java.foreign.Library;
import java.foreign.Libraries;
import java.foreign.Scope;
import java.foreign.annotations.NativeHeader;
import java.foreign.annotations.NativeFunction;
import java.foreign.annotations.NativeStruct;
import java.foreign.annotations.NativeGetter;
import java.foreign.memory.LayoutType;
import java.foreign.memory.Pointer;
import java.foreign.memory.Struct;
import java.lang.invoke.MethodHandles;
public class App {
@NativeStruct("[i32(x) i32(y)](Point)")
interface Point extends Struct<Point> {
@NativeGetter("x")
int x();
@NativeGetter("y")
int y();
}
@NativeHeader
interface Mover {
@NativeFunction("(u64:[i32 i32])v")
void move(Pointer<Point> point);
}
public static void main(String[] args) {
Library lib = Libraries.loadLibrary(MethodHandles.lookup(), "mover");
Mover mover = Libraries.bind(Mover.class, lib);
try (Scope scope = Scope.newNativeScope()) {
Pointer<Point> point = scope.allocate(
LayoutType.ofStruct(Point.class));
mover.move(point);
System.out.printf("X: %d Y: %d%n", point.get().x(),
point.get().y());
}
}
}
Интерес здесь представляет то, как объявляется интерфейс структуры и как под неё выделяется память. Обратите внимание, что в ldl-объявлении появился новый элемент — значения в круглых скобках после меток. Это аннотация метки в сокращённой форме. Полная форма выглядела бы так:
i32(name=x)
. Аннотация метки позволяет соотнести её с методом интерфейса.Прежде, чем переходить к обещанному в заголовке, осталось осветить ещё один способ взаимодействия с нативным кодом. Все предыдущие примеры вызывали нативные функции, но иногда нативному коду требуется вызывать java-код. Например, если мы хотим отсортировать массив с помощью
qsort
, нам понадобиться callback.import java.foreign.Library;
import java.foreign.Libraries;
import java.foreign.NativeTypes;
import java.foreign.Scope;
import java.foreign.annotations.NativeHeader;
import java.foreign.annotations.NativeFunction;
import java.foreign.annotations.NativeCallback;
import java.foreign.memory.Array;
import java.foreign.memory.Callback;
import java.foreign.memory.Pointer;
import java.lang.invoke.MethodHandles;
public class App {
@NativeHeader
interface StdLib {
@NativeFunction("(u64:[0i32] i32 i32 u64:(u64:i32 u64:i32) i32)v")
void qsort(Pointer<Integer> base, int nitems, int size,
Callback<QComparator> comparator);
@NativeCallback("(u64:i32 u64:i32)i32")
interface QComparator {
int compare(Pointer<Integer> p1, Pointer<Integer> p2);
}
}
public static void main(String[] args) {
Library lib = Libraries.loadLibrary(MethodHandles.lookup(), "msvcr120");
StdLib stdLib = Libraries.bind(StdLib.class, lib);
try (Scope scope = Scope.newNativeScope()) {
Array<Integer> array = scope.allocateArray(NativeTypes.INT,
new int[] { 23, 15, 4, 16, 42, 8 });
Callback<StdLib.QComparator> cb = scope.allocateCallback(
(p1, p2) -> p1.get() - p2.get());
stdLib.qsort(array.elementPointer(), (int) array.length(),
Integer.BYTES, cb);
for (int i = 0; i < array.length(); i++) {
System.out.printf("%d ", array.get(i));
}
System.out.println();
}
}
}
Легко заметить, что ldl-объявления, и так не особо простые для восприятия, быстро превращаются в зубодробительные конструкции. А ведь
qsort
— не самая сложная функция. Кроме того, в реальных библиотеках могут быть десятки структур и десятки функций, писать для них интерфейсы — дело неблагодарное. К счастью, обе проблемы легко решаются использованием утилиты jextract
, которая сгенерирует всех необходимые интерфейсы на основе заголовочных файлов. Вернёмся к первому примеру и обработаем его этой утилитой.jextract -L . -l adder -o adder.jar -t "com.example" adder.h
// импорт jextract'нутых "заголовочных" классов
import static com.example.adder_h.*;
public class Example {
public static void main(String[] args) {
System.out.println(add(3, 5));
}
}
И используем полученный jar-файл для сборки и запуска java-кода:
javac -cp adder.jar Example.java
java -cp .;adder.jar Example
Пока не особенно впечатляет, но позволяет понять принцип. А теперь проделаем то же самое с python37.dll (наконец-то!)
import java.foreign.Scope;
import java.foreign.memory.Pointer;
import static org.python.Python_h.*;
import static org.python.pylifecycle_h.*;
import static org.python.pythonrun_h.*;
public class App {
public static void main(String[] args) {
Py_Initialize();
try (Scope s = Scope.newNativeScope()) {
PyRun_SimpleStringFlags(
s.allocateCString("print(sum([23, 15, 4, 16, 42, 8]))\n"),
Pointer.nullPointer());
}
Py_Finalize();
}
}
Генерируем интерфейсы:
jextract -L "C:\Python37" -l python37 -o python.jar -t "org.python" --record-library-path C:\Python37\include\Python.h
Компилируем и запускаем:
javac -cp python.jar App.java
java -cp .;python.jar App
Поздравляю, ваше java-приложение только что загрузило в себя интерпретатор Python и выполнило в нём скрипт!
Больше примеров можно посмотреть в инструкции для первопроходцев.
Maven-проекты с примерами из статьи можно найти на GitHub.
P.S. API сейчас находится в стадии бурных изменений. В презентациях вышедших пару месяцев назад докладов легко увидеть код, который не будет компилироваться. Не застрахованы от этого и примеры из этой статьи. Если вы столкнётесь с этим, отправьте мне сообщение, постараюсь исправить.
Комментарии (9)
Moxa
15.02.2019 23:13А есть какие-нибудь бенчмарки, насколько оно быстрее чем jni?
stuf4ik
16.02.2019 00:12+1Есть хорошее видео от JVM разработчика, который принимает участие в этом проекте. И хорошо проходит по теме «native» вызовов/памяти. На 29 минуте есть небольшое сравнение между JNI и «улучшенным вариантов».
https://www.youtube.com/watch?v=sFxrjGTnvBs
sergey-gornostaev Автор
16.02.2019 08:44Пока рано измерять, так как Panama на ранних стадиях разработки. В Early-Access билдах Panama не совсем настоящая, foreign function calls делаются поверх JNI, так же как это делает JNA. Но одна из основных целей, поставленных перед проектом с самого начала — быть быстрее JNI и sun.misc.Unsafe. Маурицио Чимадаморе в докладе "Project Panama’s Foreign API" рассказывает, что в те редкие моменты, когда боевой бэкенд не падает объятый пламенем, он показывает результаты в несколько раз лучшие, чем JNI, и есть потенциал для ускорения в десятки раз.
DollaR84
Извиняюсь спросить, но зачем нужно такое скрещивание? Если только для того, чтоб на java нарисовать интерфейс программы, то у python есть куча вариантов интерфейсов, например WX widgets, или другие. Если чтоб запускать без установки на других компах pythonа, то всегда можно собрать exe-шник pyinstaller или еще чем. А вот для чего запускать python приложение из java приложения — не могу придумать вообще.
sergey-gornostaev Автор
Работа с Python — это просто пример. Проект Panama позволяет работать с любым нативным кодом, включая пакеты линейной алгебры, написанные на Fortran, графические библиотеки, типа OpenGL, библиотеки машинного обучения, типа Tensorflow, криптографические библиотеки, системные и т.п. Не всё можно реализовать на Java, и не всегда реализации на Java получаются в достаточной степени производительными, поэтому невозможно обойтись без интероперабельности с нативным кодом. Раньше для этого применялся JNI, но он слишком сложный и недостаточно быстрый. Проект Panama исправляет оба этих недостатка.
DollaR84
А, понял, спасибо за разъяснение. После статьи как-то не уловил данных возможностей.
Suvitruf
Насколько я помню, при работе с JNI много ресурсов тратится именно на бридж между Java и нейтивом. А как тут эту проблему решили?
sergey-gornostaev Автор
Честно говоря, с деталями не знаком. Документации пока очень мало, а в код я ещё не погружался. Но судя по прослушанным докладам, к этому вопросу комплексный подход, JVM изменяют под «Панаму» сразу во многих местах — не такая тесная интеграция нативного кода с JVM, передача в нативный код только off-heap данных, большее количество информации о нативных вызовах у JIT-компилятора и т.д. и т.п.
oldd
Приведу пример из своей работы. Нужно строить дерево какого-то железа (эта железка может иметь этих детей в таком-то количестве, а вот эта- совсем других детей и.т.д.) Железо на момент изготовления программы неизвестно, что куда подключается- неизвестно. Сначала пытались выехать на описалове и настройках, но уж больно всё сложно всё получалось, и потому решили внедрить скрипты в описалово железа. И всё прям замечательно получилось — считываешь json-ку, находишь по id нужную железку, запускаешь скрипт и получаешь, что к этой железке можно подключить, а что-нет. Для скриптов используем groovy