Pull to refresh

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

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

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 сейчас находится в стадии бурных изменений. В презентациях вышедших пару месяцев назад докладов легко увидеть код, который не будет компилироваться. Не застрахованы от этого и примеры из этой статьи. Если вы столкнётесь с этим, отправьте мне сообщение, постараюсь исправить.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 25: ↑21 and ↓4+17
Comments9

Articles