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

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

    ByteBuffer よりも使い勝手が良い • sun.misc.Unsafe よりも安全 Javaプロセスのメモリ Foreign Memory API ヒープ (GCで管理されたメモリ領域) メタス ペース オフヒープ (GC管理外の メモリ領域)
  3. 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) ← イマココ
  4. 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); }
  5. 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();
  6. Step.2 関数シンボルの取得 • ライブラリから関数シンボル(位置)を取得 • C言語の標準ライブラリ関数は、 linker.defaultLookup() で取得する • SymboLookup#lookup

    メソッドの戻り値は Optional<MemorySegment> • 関数がないと、Optionl.empty() が返ってくる • 安全にエラーハンドリングできる MemorySegment symbol = linker.defaultLookup().lookup("strlen").orElseThrow();
  7. 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)
  8. Step.5 文字列のアロケート • 文字列を、オフヒープにアロケートする • どういうこと…? • Java のGCが管理するメモリ領域(ヒープ)とは別のメモ リ領域(オフヒープ)に、メモリを確保する

    • そこに、UTF-8 に変換したC言語の文字列(¥0 終端文字 列)を書き込む • ヒープ領域のデータは外部関数に渡せないので、 いったんオフヒープ領域にコピーする MemorySegment cString = SegmentAllocator.implicitAllocator().allocateUtf8String("Hello");
  9. 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); }
  10. 関数シンボルの取得方法 • ライブラリをロード • 明示的に、ライブラリの名前を指定 • 今回は、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();
  11. 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");
  12. エンディアンに注意 • 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
  13. 100% Pure Java ではなくなる • 普通のJava プログラム • Java が対応していれば、どこでも動く

    • Write once, Run anywhere • どのOSでも、どのCPUでも同じように動く • Foreign Function API を使ったプログラム • Java と依存ライブラリが対応していれば、動く • クロスプラットフォームに対応したライブラリも増えて いるので、あまり心配はいらない…? • Go や Rust のように、クロスプラットフォームにコンパイ ルできる言語も増えてきた
  14. • オフヒープデータを操作するAPI • オフヒープとは… • いままでも扱えた • ByteBuffer#allocateDirect(capacity) • FileChannel#map(FileChannel.MapMode,

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

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

    • 速い • 構造化されたデータを扱いやすい • 安全 デメリットがすべて解消された!
  17. サンプルコード: クラスファイルを読み込む 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; …
  18. サンプルコード: クラスファイルを読み込む 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を取得​)
  19. サンプルコード: クラスファイルを読み込む 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);
  20. 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);
  21. 関連資料 • 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