Встраиваем интерпретатор Python в java-приложение с помощью проекта Panama

    Пару дней назад увидел твит Брайана Гетца, но только сегодня дошли руки поиграться с примерами.

    image

    Про это и хочу кратко рассказать.

    О проекте 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 и выполнило в нём скрипт!

    image

    Больше примеров можно посмотреть в инструкции для первопроходцев.

    Maven-проекты с примерами из статьи можно найти на GitHub.

    P.S. API сейчас находится в стадии бурных изменений. В презентациях вышедших пару месяцев назад докладов легко увидеть код, который не будет компилироваться. Не застрахованы от этого и примеры из этой статьи. Если вы столкнётесь с этим, отправьте мне сообщение, постараюсь исправить.
    • +17
    • 3,7k
    • 9
    Поддержать автора
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 9

      0
      Извиняюсь спросить, но зачем нужно такое скрещивание? Если только для того, чтоб на java нарисовать интерфейс программы, то у python есть куча вариантов интерфейсов, например WX widgets, или другие. Если чтоб запускать без установки на других компах pythonа, то всегда можно собрать exe-шник pyinstaller или еще чем. А вот для чего запускать python приложение из java приложения — не могу придумать вообще.
        +4
        Работа с Python — это просто пример. Проект Panama позволяет работать с любым нативным кодом, включая пакеты линейной алгебры, написанные на Fortran, графические библиотеки, типа OpenGL, библиотеки машинного обучения, типа Tensorflow, криптографические библиотеки, системные и т.п. Не всё можно реализовать на Java, и не всегда реализации на Java получаются в достаточной степени производительными, поэтому невозможно обойтись без интероперабельности с нативным кодом. Раньше для этого применялся JNI, но он слишком сложный и недостаточно быстрый. Проект Panama исправляет оба этих недостатка.
          0
          А, понял, спасибо за разъяснение. После статьи как-то не уловил данных возможностей.
            0
            Насколько я помню, при работе с JNI много ресурсов тратится именно на бридж между Java и нейтивом. А как тут эту проблему решили?
              0
              Честно говоря, с деталями не знаком. Документации пока очень мало, а в код я ещё не погружался. Но судя по прослушанным докладам, к этому вопросу комплексный подход, JVM изменяют под «Панаму» сразу во многих местах — не такая тесная интеграция нативного кода с JVM, передача в нативный код только off-heap данных, большее количество информации о нативных вызовах у JIT-компилятора и т.д. и т.п.
            0
            Приведу пример из своей работы. Нужно строить дерево какого-то железа (эта железка может иметь этих детей в таком-то количестве, а вот эта- совсем других детей и.т.д.) Железо на момент изготовления программы неизвестно, что куда подключается- неизвестно. Сначала пытались выехать на описалове и настройках, но уж больно всё сложно всё получалось, и потому решили внедрить скрипты в описалово железа. И всё прям замечательно получилось — считываешь json-ку, находишь по id нужную железку, запускаешь скрипт и получаешь, что к этой железке можно подключить, а что-нет. Для скриптов используем groovy
            0

            А есть какие-нибудь бенчмарки, насколько оно быстрее чем jni?

              +1
              Есть хорошее видео от JVM разработчика, который принимает участие в этом проекте. И хорошо проходит по теме «native» вызовов/памяти. На 29 минуте есть небольшое сравнение между JNI и «улучшенным вариантов».
              https://www.youtube.com/watch?v=sFxrjGTnvBs
                0
                Пока рано измерять, так как Panama на ранних стадиях разработки. В Early-Access билдах Panama не совсем настоящая, foreign function calls делаются поверх JNI, так же как это делает JNA. Но одна из основных целей, поставленных перед проектом с самого начала — быть быстрее JNI и sun.misc.Unsafe. Маурицио Чимадаморе в докладе "Project Panama’s Foreign API" рассказывает, что в те редкие моменты, когда боевой бэкенд не падает объятый пламенем, он показывает результаты в несколько раз лучшие, чем JNI, и есть потенциал для ускорения в десятки раз.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое