1
0

JEP454 FFM APIの基本的な使い方

Last updated at Posted at 2024-04-28

本記事では、JEP 454: Foreign Function & Memory APIを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)からですが、それ以前は2つのIncubatorとして開発されていました。

そして、FFM APIはProject Panamaの成果です。

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

他には、Vector APIもやっています。(JDK23でEighth Incubator JEP469

実行環境

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を自分でビルドしたもの)

$ 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メソッドがライブラリを探す場所は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をクラッシュさせる可能性があるなど、危険な側面を持っています。なので、そういったメソッドを使用している場合、上記のオプションをつけなければ警告が出るようになっています。

参考:使用が制限されるメソッド

ポイント3

Arenaというクラスでヒープ外のメモリ領域の管理を行います。詳しくは後日別の記事に書くかもしれません。

ポイント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メソッドがライブラリを探す場所はOSごとに異なり、WindowsではLoadLibrary関数の仕様に依存します。

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

参考:パッケージ化されていないアプリの標準検索順序

ポイント2

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

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

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

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

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

このようなことが起こるのは、ポイント1で述べたように、libraryLookupメソッドが行うライブラリのロードはOS毎の関数に依存しているためだと考えられます。

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

まとめ

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

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0