JEP454 FFM APIの基本的な使い方

本記事では、[JEP 454: Foreign Function & Memory API](https://openjdk.org/jeps/454)をJNIと比較しながら、Linux、Windowsでの基本的な使い方を紹介します。

# 概要

Foreign Function & Memory APIは頭文字をとってFFM APIと略されます。

FFM APIはJDK19から3度のプレビューを経てJDK22で正式導入されました。

FFM APIとしてJEPになったのはJDK17の[JEP 412: Foreign Function & Memory API (Incubator)](https://openjdk.org/jeps/412)からですが、それ以前は2つのIncubatorとして開発されていました。

- [JEP 370: Foreign-Memory Access API (Incubator)](https://openjdk.org/jeps/370)
Javaヒープ外へアクセスでき、java.nio.ByteBufferやsun.misc.Unsafeの代替となるAPI。

- [JEP 389: Foreign Linker API (Incubator)](https://openjdk.org/jeps/389)
Java以外の言語で書かれた関数をJavaから呼び出すことができる、JNIの代替となるAPI。

そして、FFM APIは[Project Panama](https://openjdk.org/projects/panama/)の成果です。

Project Panamaはざっくりですが、「Javaの世界」と「ネイティブの世界」のインターフェースを改善する活動をしています。

他には、Vector APIもやっています。（JDK23でEighth Incubator [JEP469](https://openjdk.org/jeps/469)）

# 実行環境

## Linux

OS
```
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=22.04
DISTRIB_CODENAME=jammy
DISTRIB_DESCRIPTION="Ubuntu 22.04.3 LTS"
```

JDK（[OpenJDK jdk22](https://github.com/openjdk/jdk22)を自分でビルドしたもの）
```
$ java --version
openjdk 22-internal 2024-03-19
OpenJDK Runtime Environment (build 22-internal-adhoc.tabatad.jdk22)
OpenJDK 64-Bit Server VM (build 22-internal-adhoc.tabatad.jdk22, mixed mode, sharing)
```

gcc
```
$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
```

## Windows

OS
```
>ver

Microsoft Windows [Version 10.0.22631.3447]
```

JDK
```
$ java --version
openjdk 22 2024-03-19
OpenJDK Runtime Environment Temurin-22+36 (build 22+36)
OpenJDK 64-Bit Server VM Temurin-22+36 (build 22+36, mixed mode, sharing)
```

gcc
```
$ gcc --version
gcc.exe (x86_64-win32-seh-rev1, Built by MinGW-Builds project) 13.1.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
```

# FFM APIとJNIを比較

Cで`Hello world`と表示する関数を用意し、それをJavaから呼び出します。

まずは、LinuxでのJNI、FFM APIを比較し、そのあとにWindowsでのJNI、FFM APIを比較します。

## Linux

### JNI

```JNI.java
public class JNI {
    private native void hello();

    static {
        System.loadLibrary("hello_jni");
    }

    public static void main(String[] args) {
        new JNI().hello();
    }
}
```

```hello_jni.c
#include <stdio.h>

#include "JNI.h"

JNIEXPORT void JNICALL Java_JNI_hello (JNIEnv *, jobject) {
    printf("Hello world\n");
}
```

```
$ javac -h . JNI.java

$ gcc -shared -o libhello_jni.so -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux hello_jni.c

$ java -Djava.library.path=. JNI
Hello world
```

### Foreign Function API

```FFM.java
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

public class FFM {
    public static void main(String[] args) {
        Linker linker = Linker.nativeLinker();

        try (Arena offHeap = Arena.ofConfined()) {
            SymbolLookup lib = SymbolLookup.libraryLookup("libhello_ffm.so", offHeap);
            MethodHandle hello = linker.downcallHandle(lib.find("hello").orElseThrow(), FunctionDescriptor.ofVoid());
            
            hello.invokeExact();
        } catch(Throwable t) {
            t.printStackTrace();
        }
    }
}
```

```hello_ffm.c
#include <stdio.h>

void hello() {
    printf("Hello world\n");
}
```

```
$ gcc -shared -o libhello_ffm.so -fPIC hello_ffm.c

$ java -Djava.library.path=. --enable-native-access=ALL-UNNAMED FFM
java.lang.IllegalArgumentException: Cannot open library: libhello_ffm.so
        at java.base/java.lang.foreign.SymbolLookup.libraryLookup(SymbolLookup.java:314)
        at java.base/java.lang.foreign.SymbolLookup.libraryLookup(SymbolLookup.java:266)
        at FFM.main(FFM.java:9)

$ export LD_LIBRARY_PATH=`pwd`

$ java --enable-native-access=ALL-UNNAMED FFM.java
Hello world

$ java FFM
WARNING: A restricted method in java.lang.foreign.SymbolLookup has been called
WARNING: java.lang.foreign.SymbolLookup::libraryLookup has been called by FFM in an unnamed module
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

Hello world
```

### ポイント1

[libraryLookup](https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/lang/foreign/SymbolLookup.html#libraryLookup(java.lang.String,java.lang.foreign.Arena))メソッドがライブラリを探す場所はOSごとに異なり、posixではdlopen関数の仕様に依存します。

dlopenは`LD_LIBRARY_PATH`に定義されているパスからライブラリを検索するので、`LD_LIBRARY_PATH`にライブラリ（今回はlibhello_ffm.so）がある場所を定義しておく必要があります。

なお、JNIのように、実行時に`-Djava.library.path=`でライブラリのパスを指定する方法では例外が発生します。

### ポイント2

`--enable-native-access=ALL-UNNAMED`

FFM APIの一部のメソッドは使い方を誤るとJVMをクラッシュさせる可能性があるなど、危険な側面を持っています。なので、そういったメソッドを使用している場合、上記のオプションをつけなければ警告が出るようになっています。

参考：[使用が制限されるメソッド](https://docs.oracle.com/en/java/javase/21/core/restricted-methods.html)

### ポイント3

[Arena](https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/lang/foreign/Arena.html)というクラスでヒープ外のメモリ領域の管理を行います。詳しくは後日別の記事に書くかもしれません。

### ポイント4

JNIと比較してFFM APIは、
- ライブラリの関数を独自の書き方をする必要がない
- `javac -h`でヘッダファイルを作成する必要がない

というメリットがあります。

## Windows

### JNI

```JNI.java
public class JNI {
    private native void hello();

    static {
        System.loadLibrary("hello_jni");
    }

    public static void main(String[] args) {
        new JNI().hello();
    }
}
```

```hello_jni.c
#include <stdio.h>

#include "JNI.h"

JNIEXPORT void JNICALL Java_JNI_hello (JNIEnv *env, jobject jo) {
    printf("Hello world\n");
}
```

```
$ javac -h . JNI.java

$ gcc -shared -o hello_jni.dll -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/win32 hello_jni.c

$ java -Djava.library.path=. JNI
Hello world
```

### Foreign Function API

```FFM.java
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;

public class FFM {
    public static void main(String[] args) {
        Linker linker = Linker.nativeLinker();

        try (Arena offHeap = Arena.ofConfined()) {
            SymbolLookup lib = SymbolLookup.libraryLookup("hello_ffm.dll", offHeap);
            MethodHandle hello = linker.downcallHandle(lib.find("hello").orElseThrow(), FunctionDescriptor.ofVoid());
            
            hello.invokeExact();
        } catch(Throwable t) {
            t.printStackTrace();
        }
    }
}
```

```hello_ffm.c
#include <stdio.h>

void hello() {
    printf("Hello world\n");
}
```

```
$ gcc -shared -o hello_ffm.dll -fPIC hello_ffm.c

$ java --enable-native-access=ALL-UNNAMED FFM.java
Hello world
```

### ポイント1

[libraryLookup](https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/lang/foreign/SymbolLookup.html#libraryLookup(java.lang.String,java.lang.foreign.Arena))メソッドがライブラリを探す場所はOSごとに異なり、WindowsではLoadLibrary関数の仕様に依存します。

LoadLibrary関数はカレントディレクトリにライブラリのファイル（今回はhello_ffm.dll）があれば読み込みます。

参考：[パッケージ化されていないアプリの標準検索順序](https://learn.microsoft.com/ja-jp/windows/win32/dlls/dynamic-link-library-search-order#standard-search-order-for-unpackaged-apps)

### ポイント2

jniの場合はLinux、Windowsに関わらずjavaのソースは全く同じで実行できますが、FFMの場合、ライブラリをロードするときのファイル名の指定方法がOSで異なります。

Linuxの場合、ライブラリのファイル名を省略せずに指定する必要があります。

```java
SymbolLookup lib = SymbolLookup.libraryLookup("libhello_ffm.so", offHeap);
```

Windowsの場合、拡張子を省略してもライブラリを読み込むことができます。

```java
SymbolLookup lib = SymbolLookup.libraryLookup("hello_ffm", offHeap);
// もしくは
SymbolLookup lib = SymbolLookup.libraryLookup("hello_ffm.dll", offHeap);
```

このようなことが起こるのは、ポイント1で述べたように、[libraryLookup](https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/lang/foreign/SymbolLookup.html#libraryLookup(java.lang.String,java.lang.foreign.Arena))メソッドが行うライブラリのロードはOS毎の関数に依存しているためだと考えられます。

つまり、posixのdlopen関数は拡張子やファイル名の頭の`lib`の省略を許さず、WindowsのLoadLibrary関数は拡張子の省略を許しています。

# まとめ

本記事では、[JEP 454: Foreign Function & Memory API](https://openjdk.org/jeps/454)をJNIと比較しながら、Linux、Windowsでの基本的な使い方を紹介しました。ご参考になれば幸いです。

また、細かい表現の誤りや誤った内容を記載している可能性があります。
その際はご指摘いただけますと幸いです。