$30 off During Our Annual Pro Sale. View details »

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/
  2. 質疑応答

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

    API)
  4. • ネイティブコード(外部関数)を、 Java から直接呼び出せるようになる! • 極力、JVMがクラッシュしないようになっている • パフォーマンスの考慮もされている • 使えるようになるもの(例)

    • C 標準ライブラリ関数 • LLVM • libcurl • Win32 API • Visual J++ の J/Direct ​と同じことができる! • その他、独自のライブラリ • 言語は問わない(C, Go, Rust など) • ネイティブライブラリとしてコンパイルできる言語なら、なんでも 可 Foreign Function API
  5. • オフヒープデータを操作するAPI • オフヒープとは… • mmap(メモリ上のデータ以外にメモリアドレスを 割り当てて操作する)もここ • 今までも扱えたけど… •

    ByteBuffer よりも使い勝手が良い • sun.misc.Unsafe よりも安全 Javaプロセスのメモリ Foreign Memory API ヒープ (GCで管理されたメモリ領域) メタス ペース オフヒープ (GC管理外の メモリ領域)
  6. Q. いつから使えますか? A. 一応、もう使えます!

  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) ← イマココ
  8. Q. でも、難しいんでしょ? A. いえ、簡単です! まずは Foreign Function API を実際に使ってみましょう!

  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); }
  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();
  11. Step.2 関数シンボルの取得 • ライブラリから関数シンボル(位置)を取得 • C言語の標準ライブラリ関数は、 linker.defaultLookup() で取得する • SymboLookup#lookup

    メソッドの戻り値は Optional<MemorySegment> • 関数がないと、Optionl.empty() が返ってくる • 安全にエラーハンドリングできる MemorySegment symbol = linker.defaultLookup().lookup("strlen").orElseThrow();
  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)
  13. Step.4 メソッドハンドルの取得 • 関数とその定義を渡して、Java のメソッドを動 的に生成 • MethodHandle(動的関数呼び出し)は、JVM がい い感じに最適化してくれる

    MethodHandle strlen = linker.downcallHandle(symbol, descriptor);
  14. Step.5 文字列のアロケート • 文字列を、オフヒープにアロケートする • どういうこと…? • Java のGCが管理するメモリ領域(ヒープ)とは別のメモ リ領域(オフヒープ)に、メモリを確保する

    • そこに、UTF-8 に変換したC言語の文字列(¥0 終端文字 列)を書き込む • ヒープ領域のデータは外部関数に渡せないので、 いったんオフヒープ領域にコピーする MemorySegment cString = SegmentAllocator.implicitAllocator().allocateUtf8String("Hello");
  15. Step.6 実行して結果を取得 • メソッドハンドルを実行 • 要するに、strlen 関数を呼び出す • 戻り値を取得して、標準出力へ long

    len = (long) strlen.invoke(cString); System.out.println(len);
  16. 実行結果 • "Hello" の文字数が取得できた > java --enable-preview --source 19 StrLen.java

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

  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); }
  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();
  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");
  21. 実行結果 • 派手に文字化け…

  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
  23. 実行結果 • 無事に表示できた!

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

  25. 100% Pure Java ではなくなる • 普通のJava プログラム • Java が対応していれば、どこでも動く

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

  27. • オフヒープデータを操作するAPI • オフヒープとは… • いままでも扱えた • ByteBuffer#allocateDirect(capacity) • FileChannel#map(FileChannel.MapMode,

    position, size) • sun.misc.Unsafe#allocateMemory(capacity) • ただ、いくつか問題があった Javaプロセスのメモリ Foreign Memory API ヒープ (GCで管理されたメモリ領域) メタス ペース オフヒープ (GC管理外の メモリ領域)
  28. 既存の問題点 • ByteBuffer • メモリを開放するタイミングがGC次第 • オフヒープメモリは、それだと困る • 2GB までしか対応していない

    • あまり速くない • 扱いずらい • sun.misc.Unsafe • 危険 • 実装依存(仕様が定まっていない) • ByteBuffer の問題を解消できるので、やむを得ず使われて きた
  29. Foreign Memory API の利点 • java.lang.foriegn.* • メモリを開放するタイミングをコントロールできる • 2GBの制限がない

    • 速い • 構造化されたデータを扱いやすい • 安全 デメリットがすべて解消された!
  30. Q. どうやって使うの…? A. サンプルコードをお見せします。 mmap 使ってクラスファイルを読み込んでみましょう!

  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; …
  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を取得​)
  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);
  34. Q. やっぱり難しい…!! A. jextract というツールを使うと、簡単に使えます!

  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);
  36. まとめ • Foreign Function & Memory API で、Java の可能性が広がる! •

    ぜひ、遊んでみてく ださい!
  37. JEP 424 Foreign Function & Memory API を 試しに使ってみました! @YujiSoftware

    https://yuji.software/
  38. 質疑応答

  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