Upgrade to Pro — share decks privately, control downloads, hide ads and more …

JEP 424 Foreign Function & Memory API を試しに使ってみました!

YujiSoftware
September 30, 2022

JEP 424 Foreign Function & Memory API を試しに使ってみました!

2022年9月30日のJJUGナイトセミナー「Java 19 リリース記念イベント」の資料です。

参考: JEP 424: Foreign Function & Memory API (Second Incubator)
ソースコード: https://github.com/YujiSoftware/ffm

YujiSoftware

September 30, 2022
Tweet

More Decks by YujiSoftware

Other Decks in Technology

Transcript

  1. JEP 424
    Foreign Function &
    Memory API を
    試しに使ってみました!
    @YujiSoftware
    https://yuji.software/ffm/

    View Slide

  2. 質疑応答

    View Slide

  3. Q.
    Foreign Function & Memory
    API って何ですか?
    A.
    外部関数や外部メモリを扱えるようにする、新しいAPIです!
    (略して、FFM API)

    View Slide

  4. • ネイティブコード(外部関数)を、
    Java から直接呼び出せるようになる!
    • 極力、JVMがクラッシュしないようになっている
    • パフォーマンスの考慮もされている
    • 使えるようになるもの(例)
    • C 標準ライブラリ関数
    • LLVM
    • libcurl
    • Win32 API
    • Visual J++ の J/Direct ​と同じことができる!
    • その他、独自のライブラリ
    • 言語は問わない(C, Go, Rust など)
    • ネイティブライブラリとしてコンパイルできる言語なら、なんでも

    Foreign Function API

    View Slide

  5. • オフヒープデータを操作するAPI
    • オフヒープとは…
    • mmap(メモリ上のデータ以外にメモリアドレスを
    割り当てて操作する)もここ
    • 今までも扱えたけど…
    • ByteBuffer よりも使い勝手が良い
    • sun.misc.Unsafe よりも安全
    Javaプロセスのメモリ
    Foreign Memory API
    ヒープ
    (GCで管理されたメモリ領域)
    メタス
    ペース
    オフヒープ
    (GC管理外の
    メモリ領域)

    View Slide

  6. Q.
    いつから使えますか?
    A.
    一応、もう使えます!

    View Slide

  7. FFM API の歴史
    • 2014年ごろ
    • Project Panama スタート
    • JEP 191: Foreign Function Interface
    • Java 14 (2020/3/17)
    • JEP 370: Foreign-Memory Access API (Incubator)
    • Java 15 (2020/9/15)
    • JEP 383: Foreign-Memory Access API (Second Incubator)
    • Java 16 (2021/3/16)
    • JEP 393: Foreign-Memory Access API (Third Incubator)
    • JEP 389: Foreign Linker API (Incubator)
    • Java 17 (2021/9/14)
    • JEP 412: Foreign Function & Memory API (Incubator)
    • Java 18 (2022/3/22)
    • JEP 419: Foreign Function & Memory API (Second Incubator)
    • Java 19 (2022/9/20)
    • JEP 424: Foreign Function & Memory API (Preview) ← イマココ

    View Slide

  8. Q.
    でも、難しいんでしょ?
    A.
    いえ、簡単です!
    まずは Foreign Function API を実際に使ってみましょう!

    View Slide

  9. C言語の標準関数を呼ぶ
    • strlen 関数
    • 文字列の長さを調べるC言語の標準ライブラリ関数
    • Java の String#length() と同等
    public static void main(String[] args) throws Throwable {
    Linker linker = Linker.nativeLinker();
    MemorySegment symbol =
    linker.defaultLookup().lookup("strlen").orElseThrow();
    FunctionDescriptor descriptor =
    FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS);
    MethodHandle strlen =
    linker.downcallHandle(symbol, descriptor);
    MemorySegment cString =
    SegmentAllocator.implicitAllocator().allocateUtf8String("Hello");
    long len = (long) strlen.invoke(cString);
    System.out.println(len);
    }

    View Slide

  10. Step.1 リンカーの取得
    • Javaのコードと外部関数の相互アクセスを提供
    • CPU / OS 依存の処理を自動的に行う
    • 対応環境
    • Microsoft x64 ABI (Windows on x64)
    • System V AMD64 ABI (Linux on x64, Intel Mac)
    • macOS AArch64 (M1 mac)
    • AAPCS ABI (Linux on Arm64)
    • 対応外の環境で動かすと実行時に例外が飛ぶ
    • java.lang.UnsupportedOperationException:
    Unsupported os, arch, or address size
    Linker linker = Linker.nativeLinker();

    View Slide

  11. Step.2 関数シンボルの取得
    • ライブラリから関数シンボル(位置)を取得
    • C言語の標準ライブラリ関数は、
    linker.defaultLookup() で取得する
    • SymboLookup#lookup メソッドの戻り値は
    Optional
    • 関数がないと、Optionl.empty() が返ってくる
    • 安全にエラーハンドリングできる
    MemorySegment symbol =
    linker.defaultLookup().lookup("strlen").orElseThrow();

    View Slide

  12. Step.3 関数の定義を設定
    • 引数と戻り値を定義
    • size_t strlen(const char *s);
    • 戻り値:長さ(size_t)
    • 64bit Java では、ValueLayout.JAVA_LONGを指定
    • 引数:文字列 (const char *s)
    • C言語の文字列は、配列へのポインターとして扱う
    • Java では、ValueLayout.ADDRESSを指定
    FunctionDescriptor descriptor =
    FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)

    View Slide

  13. Step.4 メソッドハンドルの取得
    • 関数とその定義を渡して、Java のメソッドを動
    的に生成
    • MethodHandle(動的関数呼び出し)は、JVM がい
    い感じに最適化してくれる
    MethodHandle strlen =
    linker.downcallHandle(symbol, descriptor);

    View Slide

  14. Step.5 文字列のアロケート
    • 文字列を、オフヒープにアロケートする
    • どういうこと…?
    • Java のGCが管理するメモリ領域(ヒープ)とは別のメモ
    リ領域(オフヒープ)に、メモリを確保する
    • そこに、UTF-8 に変換したC言語の文字列(¥0 終端文字
    列)を書き込む
    • ヒープ領域のデータは外部関数に渡せないので、
    いったんオフヒープ領域にコピーする
    MemorySegment cString =
    SegmentAllocator.implicitAllocator().allocateUtf8String("Hello");

    View Slide

  15. Step.6 実行して結果を取得
    • メソッドハンドルを実行
    • 要するに、strlen 関数を呼び出す
    • 戻り値を取得して、標準出力へ
    long len = (long) strlen.invoke(cString);
    System.out.println(len);

    View Slide

  16. 実行結果
    • "Hello" の文字数が取得できた
    > java --enable-preview --source 19 StrLen.java
    5

    View Slide

  17. Q.
    結果が地味…!!
    A.
    続いて、派手なサンプルをお見せします!

    View Slide

  18. Win32 API のMessageBox 関数
    • メッセージとOKなどのボタンを表示する関数
    • やり方は、strlen と大体一緒
    public static void main(String[] args) throws Throwable {
    Linker linker = Linker.nativeLinker();
    SymbolLookup lookup =
    SymbolLookup.libraryLookup("User32", MemorySession.openImplicit());
    MemorySegment symbol =
    lookup.lookup("MessageBoxW").orElseThrow();
    FunctionDescriptor descriptor =
    FunctionDescriptor.of(JAVA_INT, JAVA_LONG, ADDRESS, ADDRESS, JAVA_INT);
    MethodHandle messageBox =
    linker.downcallHandle(symbol, descriptor);
    MemorySegment text = allocateUtf16String("Hello world!");
    MemorySegment caption = allocateUtf16String("Java");
    int ret = (int) messageBox.invoke(0, text, caption, MB_OKCANCEL |
    MB_ICONINFORMATION);
    System.out.println(ret);
    }

    View Slide

  19. 関数シンボルの取得方法
    • ライブラリをロード
    • 明示的に、ライブラリの名前を指定
    • 今回は、User32.dll をロードするので"User32"
    • Linux だと、libcurl.so → "curl" のように指定
    • その中にある、MessageBoxW 関数を取得
    (Unicode 版 Win32 API は、名前の末尾が W になっている)
    SymbolLookup lookup =
    SymbolLookup.libraryLookup("User32", MemorySession.openImplicit());
    MemorySegment symbol =
    lookup.lookup("MessageBoxW").orElseThrow();

    View Slide

  20. UTF-16 文字列を取得
    • Win32 API で使う文字列は UTF-16
    1. 文字列を UTF-16 バイト配列に変換
    2. メモリをアロケート
    3. そこに、バイト配列をコピー
    public static MemorySegment allocateUtf16String(String str) {
    byte[] bytes = str.getBytes(StandardCharsets.UTF_16);
    MemorySegment addr = MemorySession.openImplicit().allocate(bytes.length + 1);
    MemorySegment segment = MemorySegment.ofArray(bytes);
    addr.copyFrom(segment);
    addr.set(JAVA_BYTE, bytes.length, (byte) 0);
    return addr;
    }
    MemorySegment text = allocateUtf16String("Hello world!");
    MemorySegment caption = allocateUtf16String("Java");

    View Slide

  21. 実行結果
    • 派手に文字化け…

    View Slide

  22. エンディアンに注意
    • Java はビッグエンディアン
    • Win32 API はリトルエンディアン
    • Win32 API でStringを使う際は、リトルエンディ
    アンのUTF-16にする
    byte[] bytes = str.getBytes(StandardCharsets.UTF_16LE);
    J a v a
    ビッグ 00 4a 00 61 00 76 00 61
    リトル 4a 00 61 00 76 00 61 00

    View Slide

  23. 実行結果
    • 無事に表示できた!

    View Slide

  24. Q.
    そのサンプルコード、
    Linux や macOS で動かない…?
    A.
    はい。環境に依存します。

    View Slide

  25. 100% Pure Java ではなくなる
    • 普通のJava プログラム
    • Java が対応していれば、どこでも動く
    • Write once, Run anywhere
    • どのOSでも、どのCPUでも同じように動く
    • Foreign Function API を使ったプログラム
    • Java と依存ライブラリが対応していれば、動く
    • クロスプラットフォームに対応したライブラリも増えて
    いるので、あまり心配はいらない…?
    • Go や Rust のように、クロスプラットフォームにコンパイ
    ルできる言語も増えてきた

    View Slide

  26. Q.
    ところで
    Foriegn Memory API の話は…?
    A.
    忘れてました。
    これから説明します。

    View Slide

  27. • オフヒープデータを操作するAPI
    • オフヒープとは…
    • いままでも扱えた
    • ByteBuffer#allocateDirect(capacity)
    • FileChannel#map(FileChannel.MapMode, position, size)
    • sun.misc.Unsafe#allocateMemory(capacity)
    • ただ、いくつか問題があった
    Javaプロセスのメモリ
    Foreign Memory API
    ヒープ
    (GCで管理されたメモリ領域)
    メタス
    ペース
    オフヒープ
    (GC管理外の
    メモリ領域)

    View Slide

  28. 既存の問題点
    • ByteBuffer
    • メモリを開放するタイミングがGC次第
    • オフヒープメモリは、それだと困る
    • 2GB までしか対応していない
    • あまり速くない
    • 扱いずらい
    • sun.misc.Unsafe
    • 危険
    • 実装依存(仕様が定まっていない)
    • ByteBuffer の問題を解消できるので、やむを得ず使われて
    きた

    View Slide

  29. Foreign Memory API の利点
    • java.lang.foriegn.*
    • メモリを開放するタイミングをコントロールできる
    • 2GBの制限がない
    • 速い
    • 構造化されたデータを扱いやすい
    • 安全
    デメリットがすべて解消された!

    View Slide

  30. Q.
    どうやって使うの…?
    A.
    サンプルコードをお見せします。
    mmap 使ってクラスファイルを読み込んでみましょう!

    View Slide

  31. サンプルコード:
    クラスファイルを読み込む
    1. 構造体の定義 (GroupLayout) を用意する
    • 構造体から値を取得する VarHandle も用意
    import static java.lang.foreign.ValueLayout.*;
    private static final GroupLayout CLASS_LAYOUT = MemoryLayout.structLayout(
    JAVA_INT.withOrder(ByteOrder.BIG_ENDIAN).withName("magic"),
    JAVA_SHORT.withOrder(ByteOrder.BIG_ENDIAN).withName("minor_version"),
    JAVA_SHORT.withOrder(ByteOrder.BIG_ENDIAN).withName("major_version")
    );
    private static final VarHandle MAGIC =
    CLASS_LAYOUT.varHandle(PathElement.groupElement("magic"));
    private static final VarHandle MINOR_VERSION =
    CLASS_LAYOUT.varHandle(PathElement.groupElement("minor_version"));
    private static final VarHandle MAJOR_VERSION =
    CLASS_LAYOUT.varHandle(PathElement.groupElement("major_version"));
    ClassFile {
    u4 magic;
    u2 minor_version;
    u2 major_version;

    View Slide

  32. サンプルコード:
    クラスファイルを読み込む
    2. FileChannel#map で開く
    try (FileInputStream stream = new FileInputStream("ClassReader.class");
    FileChannel channel = stream.getChannel()) {
    try (MemorySession session = MemorySession.openConfined()) {
    MemorySegment segment = channel.map(FileChannel.MapMode.READ_ONLY, 0, 8, session);
    ファイルを開いて、チャネルを取得
    MemorySession(メモリのライフサイクル管理クラス)を取得
    (AutoCloseable なので、try-with-resources が使える!)
    session に紐づけて、ファイルをメモリーにマップ(mmapを取得​)

    View Slide

  33. サンプルコード:
    クラスファイルを読み込む
    3. mmap から、値を読み取る
    • VarHandle#get(Object) を使う
    実行結果:
    magic=CAFEBABE, minor=-1, major=63
    int magic = (int) MAGIC.get(segment);
    short minor = (short) MINOR_VERSION.get(segment);
    short major = (short) MAJOR_VERSION.get(segment);
    System.out.format("magic=%02X, minor=%d, major=%d¥n", magic, minor, major);

    View Slide

  34. Q.
    やっぱり難しい…!!
    A.
    jextract というツールを使うと、簡単に使えます!

    View Slide

  35. jextract とは?
    • ヘッダーファイル(C言語の .h ファイル)から
    Java バインディングを機械的に生成するツール
    • https://github.com/openjdk/jextract
    • Java で書かれている
    • Foreign Function & Memory API を使って libclang を呼び出し
    ている
    • 普通の static メソッドのように呼び出せる
    • MethodHandle などの処理を隠蔽してくれる
    String cString =
    SegmentAllocator.implicitAllocator().allocateUtf8String("Hello");
    long len = strlen(cString);
    System.out.println(len);

    View Slide

  36. まとめ
    • Foreign Function &
    Memory API で、Java
    の可能性が広がる!
    • ぜひ、遊んでみてく
    ださい!

    View Slide

  37. JEP 424
    Foreign Function &
    Memory API を
    試しに使ってみました!
    @YujiSoftware
    https://yuji.software/

    View Slide

  38. 質疑応答

    View Slide

  39. 関連資料
    • Project Panama: Interconnecting JVM and native
    code
    https://openjdk.org/projects/panama/
    • jextract samples
    https://github.com/openjdk/jextract/tree/master/s
    amples
    • Foreign Function & Memory APIでハンドアセンブ
    ルしたコードを実行する (@YaSuenag)
    https://github.com/YaSuenag/garakuta/tree/maste
    r/ffm-cpumodel

    View Slide