Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

質疑応答

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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) ← イマココ

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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); }

Slide 10

Slide 10 text

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();

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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)

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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); }

Slide 19

Slide 19 text

関数シンボルの取得方法 • ライブラリをロード • 明示的に、ライブラリの名前を指定 • 今回は、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();

Slide 20

Slide 20 text

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");

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

エンディアンに注意 • 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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

サンプルコード: クラスファイルを読み込む 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; …

Slide 32

Slide 32 text

サンプルコード: クラスファイルを読み込む 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を取得​)

Slide 33

Slide 33 text

サンプルコード: クラスファイルを読み込む 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);

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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);

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

質疑応答

Slide 39

Slide 39 text

関連資料 • 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