原文はこちら。
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アプリケーションの例です。テンプレートのコードを見てみましょう。分かりやすくするために少し簡略化しています。
最初の問題は、どうやって始めるかです。これは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
ここで、SVGInputStream
はjava.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
コマンドでアプリケーションを実行してみましょう。

シンプルでありながら美しいですね。関数欄にx**2
やsin(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