Supercharge Your Java Apps with Python

原文はこちら。
The original article was written by Tim Felgentreff (Dr.rer.nat., Principal Researcher at Oracle Labs).
https://medium.com/graalvm/supercharge-your-java-apps-with-python-ec5d30634d18

GraalVMのエコシステムは、JavaScript、Ruby、Python、WebAssembly、Java、LLVM bitcodeなどの非常に興味深い言語の集まりで構成されています。これらの言語はすべて、ユニークな利点をもたらします。Python on GraalVMは、Pythonデータサイエンスライブラリの豊富なエコシステムをJava開発者に開放します。GraalVMでのPythonサポートはまだ実験的なものですが、今日からでもPythonコードやライブラリを使ってJavaアプリケーションを拡張できます。

この記事では、ポツダムにあるHasso Plattner Institute (HPI) の学生が開発した、GraalVM上でJavaからPythonライブラリを使用するためのテンプレートとして使用できるアプリケーションの例を見てみましょう。完全なソースコードは、HPI Software Architecture GroupのGitHubリポジトリで公開されています。

Java and Python Integration Example for GraalVM
https://github.com/hpi-swa-lab/graalpython-java-example
Software Architecture Group (HPI)
https://hpi.de/swa

この記事で紹介されているアプリケーションの背景、Graal Pythonの実装、そのパフォーマンスなどを知りたい方は、Using Python from Java with GraalVMの動画をご覧ください。

Using Maven to Do Python

このテンプレートは、PyGal Pythonライブラリを使って二項関数を描画するJava AWTアプリケーションの例です。テンプレートのコードを見てみましょう。分かりやすくするために少し簡略化しています。

Pygal
http://www.pygal.org/

最初の問題は、どうやって始めるかです。これはJavaアプリケーションであり、Mavenプロジェクトなので、pom.xmlを見始めるのが自然でしょう。Pythonの統合をシームレスに行うために、POMを書く際には以下の点を考慮します。

  • プロジェクトは、PythonがインストールされたGraalVM上で動作する必要がある
  • すべてのPythonの依存関係はPOMで宣言され、Javaの依存関係と同様に、Mavenの実行時にインストールされる
  • 必要なPythonパッケージとファイルは、アプリケーションのリソースに含まれている必要がある

GraalVM Makes It Possible

GraalVM上でPythonを実行する唯一のサポートされた方法は、GraalVMビルドを使用し、Pythonコンポーネントをインストールすることです。MavenプロジェクトでGraalVM上での実行を保証するのは非常に簡単です。maven-enforcer-pluginを使用して、JAVA_HOME環境変数がGraalVMのディストリビューションを指しているかどうかを確認します。これは、GraalVMツリー内のPython言語ツールと対話する際にも便利です。

<id>enforce-graalvm-python</id>
<phase>validate</phase>
<goals><goal>enforce</goal></goals>
<configuration>
  <rules>
    <requireFilesExist>
      <message>JAVA_HOME must be a GraalVM with Python installed. Download GraalVM from https://www.graalvm.org/downloads and install the Python component: `gu install python`</message>
      <files>
        <file>${env.JAVA_HOME}/bin/graalpython</file>
      </files>
    </requireFilesExist>
  </rules>
  <fail>true</fail>
</configuration>

上記構成ではJAVA_HOMEディレクトリのgraalpython実行ファイルを探し、もし見つからない場合にはエラーメッセージ付きでビルドが失敗します。

Adding the Python Package Universe

Mavenを使ったPythonパッケージのインストールはもう少し複雑で、Pythonアプリケーションが通常どのようにパッケージ化され配布されるかについて少し理解する必要があります。Pythonライブラリやパッケージは、システム全体またはユーザごとにインストールできますが、自己完結型のアプリケーションを配布してシステムの他の部分との衝突を避けたい場合、それはほとんどの場合望ましくありません。このような理由から、Pythonコミュニティは、プロジェクトのための仮想環境を作成するためにvenvというPythonモジュールを使用することを推奨しています。venvモジュールはPython 3標準ライブラリの一部です。古いPython 2バージョンのユーザーであれば、Python標準ライブラリではありませんが、同じ目的を果たすvirtualenvパッケージを覚えていらっしゃるかもしれません。

環境を作成するフォルダ名を指定してvenvモジュールを実行するだけで仮想環境を作成できます。Pythonの仮想環境は、最初はPythonランタイムにどこにパッケージをインストールしてロードするかを伝えるスクリプトとシンボリックリンクの集合体に過ぎません。一度作成された仮想環境には、PyPIからPythonパッケージを環境にインストールするために使用できるpipランチャーがbinディレクトリにあります。

PyPI – The Python Package Index
https://pypi.org/

仮想環境を準備するために、exec-maven-pluginを使用します。(先ほど確認したJAVA_HOMEにある)GraalVM Pythonに同梱されているvenvモジュールを呼び出し、pipランチャーを使用してPyGalをインストールすると、mvn generate-resourcesを実行するだけで必要なPythonパッケージをインストールできます。

<executions>
  <execution>
    <id>Prepare venv</id>
    <phase>generate-resources</phase>
    <goals><goal>exec</goal></goals>
    <configuration>
      <executable>${env.JAVA_HOME}/bin/graalpython</executable>
      <arguments>
        <argument>-m</argument>
        <argument>venv</argument>
        <argument>venv</argument>
      </arguments>
    </configuration>
  </execution>
  <execution>
    <id>Install required packages into venv</id>
    <phase>generate-resources</phase>
    <goals><goal>exec</goal></goals>
    <configuration>
      <executable>venv/bin/pip</executable>
      <arguments>
        <argument>install</argument>
        <argument>pygal==2.4.0</argument>
      </arguments>
      <environmentVariables>
        <VIRTUAL_ENV>${project.basedir}/venv</VIRTUAL_ENV>
      </environmentVariables>
    </configuration>
  </execution>
</executions>

Package Them Up

リソースバンドルはMavenにコアな関心事として組み込まれているので、Pythonの場合はそれらのリソースが何であるかを理解するだけでよいのです。仮想環境作成時に、ランチャーとインストールしたパッケージを含むフォルダ構造が作成されました。パッケージだけをバンドルして、サイズを小さくしたくなるかもしれませんが、これは得策ではありません。

Pythonとそのツールはコマンドラインを中心に構築されており、仮想環境も同様です。実際、仮想環境は実行ファイル自体をパッケージを探すための目印として使用します。これの動作方法の正確な実装に依存したくないので、すべてをバンドルしてしまいます。

<resource>
  <directory>${project.basedir}</directory>
  <includes>
    <include>venv/**/*</include>
  </includes>
</resource>

Connecting Java to Python

main関数では、SVGキャンバスと、ユーザーが数式を入力できる入力フィールドを備えたシンプルなAWTフレームを作成します。GraalVMのembedding APIを使用してPythonコンテキストを作成し、ライブラリコードをロードします。入力フィールドのコールバックでは、Python関数を呼び出して新しいSVGデータを生成し、それをSVGキャンバスにプッシュして表示しています。

Embedding Languages
https://www.graalvm.org/reference-manual/embed-languages/

Creating a Python Context

最初の興味深い点は、作成したPythonの仮想環境でembedding APIを正しく使用する方法です。そのためには、GraalVM Contextを作成する前に、いくつかのオプションを設定する必要があります。

Context context = Context.newBuilder("python").
    allowAllAccess(true).
    option("python.ForceImportSite", "true").
    option("python.Executable", VENV_EXECUTABLE).
    build();

では見ていきましょう。まず、Context.Builderを作成し、python言語が使えるようにします。(Pythonだけでなく、その他の言語も暗黙的に有効になります。例えば、PythonはC拡張サポートのために “llvm “言語に依存しています)。次に、ネイティブコードやファイルシステムなどへのすべてのアクセスを許可するフラグを設定します。今のところ、物事を進めるためにすべてのパーミッションを設定しておき、後で必要なものに絞っていけばよいでしょう。

この2つのoptionの呼び出しは密接に関連しており、Pythonの背景を知る上で必要なものです。マシン上のPython実行ファイルは、その起動コードの一部として、常にimport siteに相当するものを実行します。siteモジュールは、ユーザーパッケージとシステムパッケージのパッケージパスを設定するだけでなく、実行ファイルが仮想環境内にあるかどうかを検出し、それに応じてパッケージパスを設定する役割を果たします。

これは、Pythonの埋め込みでは必ずしも望ましいことではないので、最初のオプションであるForceImportSiteを有効にする必要があります。ここで、問題が発生します。siteモジュールはパッケージパスを決定するために起動時の実行パスを使用しますが、私たちはJavaアプリケーションを起動しています。これが2番目のオプションの目的です。Pythonランタイムに、仮想環境内の実行ファイルから起動されたかのように振る舞うように指示します。では、VENV_EXECUTABLEはどこで得られるのでしょうか?簡単です。

Main.class.getClassLoader().getResource("venv/bin/python").getPath();

Preparing the Java Code

embedding APIを直接使用して、Python空間からValueインスタンスを取得し、それらと対話することもできます。しかし、JavaとPythonのコードを切り離すためには、Javaインターフェースを使うのが合理的です。こうすることで、将来的にPythonをベースとしない他のレンダリングバックエンドを使うこともより簡単に実現できます。

interface GraphRenderer {
  InputStream render(String function, int steps);
}

いくつかのPythonコードを評価する必要がありますが、GraphRendererインターフェイスを実装した1つのPythonクラスをインポートするだけで済むように設定します。Python クラス PygalRenderer へのハンドルを取得したら、そのインスタンスを生成します。その後、オブジェクトをラップしてGraphRendererインターフェイスの実装として公開することで、開発時に少しだけ静的な型付けの恩恵を受けることができます。

Value pygalRendererClass = context.getPolyglotBindings().getMember("PygalRenderer");
Value pygalRenderer = pygalRendererClass.newInstance();
pygalRenderer.as(GraphRenderer.class);

Preparing the Python Code

さて、Javaコードがどのように見えてもらいたいか設定したので、次にPythonファイルを書いてロードしてみましょう。PyGalのAPIは我々の目的には少々低レベルすぎます。Javaで定義したGraphRendererインターフェースに合わせるために、Pythonクラスを作成し、オブジェクトインスタンスへの(Javaでは暗黙の)参照に加えて2つのパラメータを取るレンダー関数を作成します。

class PygalRenderer:
  def render(self, func, steps):

このコードでは、まず、あるズームレベルでの各ステップの値を計算する必要があります。

    values = []
    x = -100
    while x < 100:
      values.append(eval(func, globals={"x": x, **math.__dict__}))
      x += 200 / steps

ご覧の通り、ユーザーが入力フィールドに入れた文字列を評価しています。このコードではエラーチェックを行っていませんが、ユーザーは実際にPythonの式を実行している可能性があるので、実際のアプリケーションでは入力の検証をより慎重に行う必要があり、Pythonコードの機能を最小化するためにGraalVMのサンドボックス機能を使用することを検討する必要があります。

値を取得したら、PyGalを呼び出してSVGをレンダリングします。SVGデータはUTF-8エンコーディングされたPythonのbytesオブジェクトです。

    chart = pygal.Line(fill=False)
    chart.add(f"f(x) = {func}", values)
    svg_data = chart.render()

JavaインターフェースはInputStreamの戻り値を期待しているので、Pythonではjava.io.InputStreamをサブクラス化し、そのサブクラスを戻り値として利用します。

    stream = SVGInputStream()
    stream.this.bytestream = iter(svg_data)
    return stream

ここで、SVGInputStreamjava.io.InputStreamの適切なサブクラスであり、いくつかのメソッドがPythonで実装されているだけです。SVGInputStreamはPythonオブジェクトではなく、適切なJavaオブジェクトであるため、”sealed”、つまりPythonオブジェクトのように動的にメンバーを追加できない状態です。GraalVM上のPythonは、Pythonからしか見えない特別なthisメンバを提供し、そこに動的に追加メンバを定義できます。ただし、これらはPythonのコードからしか見ることができず、Javaから参照する方法はありません。

では、SVGInputStreamはどのようにして実装するのでしょうか?通常のPython構文を使用します。

class SVGInputStream(java.io.InputStream):
  def read(self, *args):
    ...

InputStream抽象クラスにはPythonで定義しなければならない abstract int read() メソッドがある点が落とし穴です。しかし、PythonはJavaのように関数のオーバーロードをサポートしていないので、Pythonのreadメソッドはread()だけでなく、デフォルトのInputStreamのread(byte[])read(byte[], int, int)の実装をオーバーライドします。そのため、Pythonの実装は3つのバリエーションを1つのメソッドで処理しなければなりません。byte配列を扱っているので、値の範囲も考慮しなければなりません(Python のバイトは符号なしですが、Java のバイトは符号ありです)。SVGInputStreamの実装の詳細については、リポジトリを参照してください。

最後に、PygalRendererをJavaコードにエクスポートする必要があります。Pythonには暗黙のグローバル名前空間はなく、すべてモジュールの中にあります(Python実行ファイルを実行したときに表示されるREPLでさえ、__main__モジュールの中にあります)。PythonのクラスをGraalVM Polyglotのグローバルな名前空間にエクスポートするには、以下のコードを使用します。

import polyglot
polyglot.export_value("PygalRenderer", PygalRenderer)

Plugging in

コンポーネントの相互作用を整理し、その周辺のコードを書いたところで、mvn exec:execコマンドでアプリケーションを実行してみましょう。

The finished AWT application rendering a PyGal graph

シンプルでありながら美しいですね。関数欄にx**2sin(x)を入力してReturnを押してください。私のマシンでは最初のレンダリングに数秒かかりますが、このときにJavaとPythonのコードのユニークな組み合わせを最適化するためにコンパイルスレッドが立ち上がります。その後のレンダリングはより高速になり、(私のマシンで)5、6回目のレンダリング後には、新しいレンダリングリクエストに対して1秒以下で描画します。すべてのコードがGraalVM JITによってコンパイルされているため、マシンの負荷は下がります。

GraalVM Native Imageを使うと、事前にJavaコードをコンパイルできます。アプリケーションが最初のレンダリングの時点で高速になるようになるよう、ウォームアップされたPythonコードも同様に永続化できるように積極的に取り組んでます。この機能のプロトタイプはすでにできており、そう遠くない将来にPython用にこの機能を実装する予定です。

Conclusions

GraalVM上で動作するJavaアプリケーションにPythonを組み込むのは簡単ですが、Pythonパッケージを適切に使用するには、いくつかの落とし穴があることを知っておく必要があります。この記事で紹介した小さなテンプレートリポジトリを使えば、誰でもすぐに使い始めることができ、いくつかの障害やスタートアップのハードルを避けることができるでしょう。ぜひお試しください。

GraalPython
https://www.graalvm.org/python

みなさんのフィードバックを常に歓迎しています。GithubやSlack、あるいは原文エントリのコメント欄からどうぞ。機能リクエストや問題提起、エコシステムのパッケージの優先順位付けなど、GraalVMをJavaとPythonのための素晴らしいランタイムにするための貴重なご意見をお待ちしています。

GitHub
https://github.com/graalvm/graalpython
GraalVM Slack Channel Invitation
https://www.graalvm.org/slack-invitation

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中