道化的プログラミング

GraalVMのnative-imageで共有ライブラリを作る

最近話題のGraalVMのnative-imageで、共有ライブラリを作れるようなので遊んでみた。

やること

おおまかに以下のとおり

  • Javaコードを書く
  • javac でコンパイル
  • native-image で共有ライブラリを生成
  • 生成したライブラリを実際に呼び出す

ディレクトリ構成

.
├── c
│   └── main.c
└── java
    └── org
        └── pkg
            └── implnative
                └── NativeImpl.java

Javaコードを書く

今回書いたのはこんな感じ。文字列をやりとりするような物にしている。

import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.graalvm.nativeimage.c.function.CEntryPoint;
import org.graalvm.nativeimage.IsolateThread;
import org.graalvm.nativeimage.c.type.CTypeConversion;
import org.graalvm.nativeimage.c.type.CTypeConversion.CCharPointerHolder;
import org.graalvm.nativeimage.c.type.CCharPointer;

public final class NativeImpl {
    @CEntryPoint(name = "Java_org_pkg_apinative_Native_add")
    static int add(IsolateThread thread, int a, int b) {
        return a + b;
    }

    @CEntryPoint(name = "Java_org_pkg_apinative_Native_rptstr")
    static CCharPointer repeatString(IsolateThread thread, CCharPointer cStr, int i) {
        String jStr = CTypeConversion.toJavaString(cStr);
        return CTypeConversion.toCString(IntStream.range(0, i).mapToObj(x -> jStr).collect(Collectors.joining())).get();
    }

    @CEntryPoint(name = "Java_org_pkg_apinative_Native_hello")
    static CCharPointer hello(IsolateThread thread) {
        return CTypeConversion.toCString("hello from native lib").get();
    }
}

CEntryPoint アノテーションを付与することで共有ライブラリに出力することができ、実際に呼び出すときの名前を指定することができる。

CEntryPoint が付与されたメソッドは以下の制約がある。これを満たさないと、共有ライブラリ化のタイミングでエラーが発生する。

  • static でなければならない
  • 引数、戻り値はプリミティブ型もしくはポインタ型で無ければならない
  • 引数に IsolateThread または Isolate 型の物を含まなければならない

なお、これらの見慣れないクラス群は、 ${GRAAL_HOME}/jre/lib/boot/graal-sdk.jar に含まれているが、GraalVM同梱のjavacでコンパイルする場合はクラスパスに指定する必要はない。
IDEで赤線を引かれないためには色々設定が必要かもしれないが今回は割愛。

javac でコンパイル

.java と同じディレクトリに .class が生成される。

${GRAAL_HOME}/bin/javac java/org/pkg/implnative/NativeImpl.java

native-image で共有ライブラリを生成

いよいよ共有ライブラリを生成する。 --shared を指定する場合、 -H:Name を要求されるので適当につけておく。

$GRAAL_HOME/bin/native-image --shared -H:Name=libnativeimpl -cp java

うまくいけば、 共有ライブラリと、それを利用するためのヘッダファイル群が生成される。 内容を見ると、CEntryPoint アノテーションで指定した名前で関数が生成されている。

int Java_org_pkg_apinative_Native_add(graal_isolatethread_t*, int, int);

char* Java_org_pkg_apinative_Native_rptstr(graal_isolatethread_t*, char*, int);

char* Java_org_pkg_apinative_Native_hello(graal_isolatethread_t*);

生成したライブラリを実際に呼び出す

ライブラリなので、単体では動かせない。ので、実際にCから呼び出してみる。

C のコード

#include <stdio.h>
#include <stdlib.h>
#include <libnativeimpl.h>

int main(void) {
  graal_isolatethread_t *thread = NULL;
  if (graal_create_isolate(NULL, NULL, &thread) != 0) {
    fprintf(stderr, "error on isolate creation or attach\n");
    return 1;
  }
  int result = Java_org_pkg_apinative_Native_add(thread, 1, 2);
  printf("%d\n", result);

  char* hello = Java_org_pkg_apinative_Native_hello(thread);
  printf("%s\n", hello);

  char* repeat = Java_org_pkg_apinative_Native_rptstr(thread, "ABC", 3);
  printf("%s\n", repeat);

  graal_tear_down_isolate(thread);
}

コンパイル & 実行

リンカオプションはCのファイル名の後に指定する必要がある(ハマった)

gcc -Wall -I ./ c/main.c -L ./ -l nativeimpl -Wl,-rpath='$ORIGIN/'

コンパイルに成功すれば、実行ファイルが生成されるはず。

./a.out
3
hello from native lib
ABCABCABC

めでたい 🎉

参考にしたもの

ソース一式

https://github.com/vertical-blank/graal-native-lib