原文はこちら。
The original article was written by Dwi Prasetyo Adi Nugroho (Master’s Student at TU Berlin).
https://towardsdatascience.com/code-in-java-execute-as-c-921f5db45f20
JavaとC++は、今でも最も人気のあるプログラミング言語で、この2つの言語には、異なるデザインと特徴があります。問題によっては、どちらか一方が他方よりも優れた動作をすることもあります。しかし、ある時点では、Javaで書かれたメソッドがC++のコードを呼び出すなど、これらの言語を統合する必要があります。JavaとC++を統合するニーズは新しいものではありません。実際、1996年に出たチュートリアルを見つけることができます。とはいえ、それ以後両言語は著しく発展しています。
Java Tip 17: Integrating Java with C++
https://www.javaworld.com/article/2077513/java-tip-17–integrating-java-with-c–.html
このチュートリアルでは、GraalVMネイティブイメージを使用してC++からjavaメソッドを呼び出す方法を学びます。GraalVMについて初めて聞いたという方は、そのホームページをチェックしてください。
GraalVM
https://www.graalvm.org/
GraalVMは、Javaアプリケーションを実行するために使用できる仮想マシンです。JVMをベースにしていますが、AOTコンパイル、少ないメモリフットプリント、その他の高度な最適化などの機能が追加されています。さらに、GraalVMは、Javaアプリケーションをネイティブアプリケーションにコンパイルするために使用できるnative-imageプラグインを提供しています。つまり、アプリケーションの実行にあたり、JVMのインストールや実行は不要です。興味深いことに、このnative-imageプラグインを使用して、Javaアプリケーションを共有ライブラリにコンパイルし、C++コードから読み込むこともできます。これは、主にC++で書かれたコードを書いているが、その一部で既存のJavaライブラリを使用したい場合に非常に便利です。
How it Works
チュートリアルをはじめる前に、動作のしくみを見ていきましょう。
- Javaでメソッドを書く
- JavaコードをGraalVMを使ってC++共有ライブラリ(yourlib.so)とヘッダーファイル(yourlib.h)にコンパイルする
- ライブラリとヘッダーファイルをC++プロジェクトに読み込む
この例では、3rdパーティーのJavaライブラリの関数をC++プロジェクトで利用したい、というユースケースをイメージしています。特に、GoogleのGuavaライブラリの数学関数のひとつを利用したいと考えています。
Guava: Google Core Libraries for Java
https://github.com/google/guava
Preparing The Java Project
まず、Javaメソッドのためにmavenプロジェクトを構成する必要があります。mavenでプロジェクトをビルドすると、依存関係のインポートだけでなく、GraalVM native-imageプラグインを使って共有ライブラリの作成も可能です。プロジェクトを作成したら、Guavaライブラリ(もしくは必要なライブラリ)とGraalVMライブラリへの依存関係をpom.xmlに追加します。
<dependencies>
<!-- https://mvnrepository.com/artifact/org.graalvm.nativeimage/svm -->
<dependency>
<groupId>org.graalvm.nativeimage</groupId>
<artifactId>svm</artifactId>
<version>19.3.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
</dependencies>
続いて、以下のプラグインを追加し、JavaコードをビルドしてC++共有ライブラリにします。
<build>
<finalName>libmymath</finalName>
<plugins>
<plugin>
<groupId>com.oracle.substratevm</groupId>
<artifactId>native-image-maven-plugin</artifactId>
<version>20.0.0</version>
<executions>
<execution>
<goals>
<goal>native-image</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<buildArgs>--shared -H:Name=libmymath</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
<finalName> タグには、生成するライブラリの名前を指定します。後に必要になるので、<version>タグで指定したバージョンに注意してください。<buildArgs>タグでは、sharedとnameのパラメータを指定します。これはGraalVM native-imageプラグインにネイティブ実行ファイルではなく共有ライブラリを生成するよう指示するパラメータです。NameパラメータにはfinalNameと同じ名前を指定します。
ではJavaのメソッドの準備をしましょう。整数xを入力とし、xより大きな2のべき乗数のうち、最もxに近いものを計算したい、としましょう。これは具体的にはxが14の場合、14より大きな2のべき乗は16なので、この例では16を返します。以下は関数のスニペットです。JavaとC++の関数を橋渡しをするIsolateThread型のスレッドを最初のパラメータとして追加する必要があることに注意してください。このパラメータは先頭になければなりません。また、メソッド名もCEntryPointアノテーションで指定する必要があります。これはC++コードで参照するメソッド名です。完全なJavaプロジェクトは以下のリポジトリで確認できます。
Repository for tutorial on embedding Java methods to C++ codes using the shared library feature of GraalVM Native Image
https://github.com/dpanugroho/graalvm-java-method-embedding
import com.google.common.math.IntMath;
import org.graalvm.nativeimage.IsolateThread;
import org.graalvm.nativeimage.c.function.CEntryPoint;
public class MyMath {
@CEntryPoint (name = "ceilingPowerOfTwo")
public static int ceilingPowerOfTwo(IsolateThread thread, int x) {
return IntMath.ceilingPowerOfTwo(x);
}
}
Setting Up GraalVM
プロジェクトを設定したので、ここからはmvn packageを実行してJavaメソッドをコンパイルし、共有ライブラリにする必要があります。しかしながら、この機能はGraalVMでのみ利用可能なので、以下のような手順でGraalVMをセットアップし、JAVA_HOMEをGraalVMバイナリに向ける必要があります。
- GraalVMをダウンロードします。適切なアーキテクチャ、Javaバージョン、pom.xmlで指定するGraalVMのバージョンを選択していることを確認してください。具体的には、このサンプルプロジェクトではGraalVM 20.0.0のJava 8を使っています。
GraalVM Community Edition Release Notes
https://github.com/graalvm/graalvm-ce-builds/releases - ネイティブイメージ拡張機能をguユーティリティを使ってインストールします。Unix環境では以下のようにします。
<path_to_graalvm_directory>graalvm-ce-java8–20.0.0/bin/gu install native-image - JAVA_HOMEをダウンロードしたGraalVM展開先のディレクトリに向けます。Unix環境では以下のようにします。
export JAVA_HOME=<path_to_graalvm_directory/graalvm-ce-java8–20.0.0>
Compile To Shared Library
これでmvn package installで共有ライブラリをビルドできるようになりました。実行すると数個のファイルが生成されます。
- graal_isolate.h
- graal_isolate_dynamic.h
- libmymath.h
- libmymath_dynamic.h
- libmymath.so (他のOSでは形式が異なります)
独自のライブラリの他に、Graal VM C++のライブラリも生成してくれます。
Import The Library To The C++ Code
では生成した共有ライブラリを以下のシンプルなC++プロジェクトで使ってみましょう。生成されたファイルをこのC++プロジェクトのディレクトリ内に配置します。例えば、includeディレクトリを作成し、その中に生成されたファイルを入れます。
#include <iostream>
#include <libmymath.h>
int main() {
graal_isolate_t *isolate = NULL;
graal_isolatethread_t *thread = NULL;
if (graal_create_isolate(NULL, &isolate, &thread) != 0) {
fprintf(stderr, "initialization error\n");
return 1;
}
printf("Result> %d\n",ceilingPowerOfTwo(thread, 14));
return 0;
}
以下のコマンドでC++コードをコンパイルし、実行してみましょう。
g++ ceilingPowerOfTwoCpp.cpp -L includes/ -I includes/ -lmymath -o ceilingPowerOfTwoCpp
訳注:以下のビルドコマンドではソースコードとしてceilingPowerOfTwoCpp.cppを使っていますが、リポジトリ上はmain.cppですので、ご注意ください。
includesディレクトリを環境変数 LD_LIBRARY_PATH に設定する必要があるかもしれません。
export LD_LIBRARY_PATH=<path_to_includes_directory>
Conclusion
プログラミング言語が同時進行で成長する世界では、相互運用性は避けて通れない要件です。GraalVM Native Imageの共有ライブラリ機能により、C++プログラムにJavaメソッドを簡単に埋め込むことができ、複雑なユーザ定義関数の記述やサードパーティ製ライブラリの利用が可能になります。例えば、高性能かつ高レベルのAPIを目指すデータ処理フレームワークの開発で有望かもしれません。
GraalVMについて詳しく知りたい方は、GraalVMのホームページをご覧ください。
Getting started with GraalVM
https://www.graalvm.org/docs/getting-started/
このチュートリアルのフルコードは、以下のURLからどうぞ。
Repository for tutorial on embedding Java methods to C++ codes using the shared library feature of GraalVM Native Image
https://github.com/dpanugroho/graalvm-java-method-embedding