13 外部関数およびメモリーAPI

外部関数およびメモリー(FFM) APIを使用すると、JavaプログラムはJavaランタイムの外部のコードおよびデータと相互運用できます。このAPIによって、JNIの脆弱性や危険性を伴わずに、Javaプログラムがネイティブ・ライブラリをコールしてネイティブ・データを処理できます。このAPIは、外部関数(JVM外のコード)を起動し、外部メモリー(JVMで管理されていないメモリー)に安全にアクセスします。

ノート:

これはプレビュー機能です。プレビュー機能は、設計、仕様および実装が完了したが、永続的でない機能です。プレビュー機能は、将来のJava SEリリースで、異なる形式で存在することもあれば、まったく存在しないこともあります。プレビュー機能が含まれているコードをコンパイルして実行するには、追加のコマンド行オプションを指定する必要があります。『Preview Language and VM Features』を参照してください。

外部関数およびメモリーAPIの背景情報は、JEP 434を参照してください。

FFM APIは、パッケージjava.lang.foreignに含まれています。

オフヒープ・メモリー

オフヒープ・データは、JVMのヒープとも呼ばれるJavaランタイム外部のメモリーに格納されるデータです。オフヒープ・データはオフヒープ・メモリーに格納され、MemorySegmentオブジェクトによって表されます。ヒープ・メモリーとは異なり、オフヒープ・メモリーは不要になった際にガベージ・コレクションの対象になりません。オフヒープ・メモリーの割当ておよび割当て解除の方法とタイミングを制御できます。

JavaアプリケーションでCなど異なる言語の関数やメソッドを呼び出すには、その引数がオフヒープ・メモリー内にある必要があります。

次の例では、オフヒープ・メモリーを割り当て、Java Stringを格納してから、オフヒープ・メモリーの内容を出力します。

    static void allocateCharArray(String s) {
        try (Arena arena = Arena.openConfined()) {

            // Allocate off-heap memory
            MemorySegment nativeText = arena.allocateUtf8String(s);

            // Access off-heap memory
            for (int i = 0; i < s.length(); i++ ) {
               System.out.print((char)nativeText.get(ValueLayout.JAVA_BYTE, i)); 
            }
            
        } // Off-heap memory is deallocated                  
    }

セグメント・スコープおよびアリーナ

すべてのメモリー・セグメントはセグメント・スコープに関連付けられ、これによって、どのスレッドがメモリー・セグメントにアクセスできるかと、いつアクセスできるかが制御されます。セグメント・スコープは、SegmentScopeオブジェクトによって表されます。allocateCharArrayの例では、アリーナ・スコープを使用します。アリーナによって、メモリー・セグメントのライフサイクルが制御されます。アリーナが閉じられると、そのスコープに関連付けられたすべてのメモリー・セグメントが無効になり、それらのバッキング・メモリー領域の割当てが解除されます。try-with-resources文を使用してアリーナが閉じられると、アリーナ・スコープは存続しなくなり、そのメモリー・セグメントが無効になります。また、セグメントを支えるメモリー領域の割当て解除は原子性に対応しています。つまり、割当て解除が完全に行われるか、まったく行われません。この結果、閉じたアリーナ・スコープに関連付けられたメモリー・セグメントにアクセスしようとすると、次の例に示すIllegalStateExceptionが表示されます:

    static void allocateCharArrayError(String s) {
        
        MemorySegment nativeText;
        try (Arena arena = Arena.openConfined()) {

            // Allocate off-heap memory
            nativeText = arena.allocateUtf8String(s);
        }
        for (int i = 0; i < s.length(); i++ ) {
            // Exception in thread "main" java.lang.IllegalStateException: Already closed
            System.out.print((char)nativeText.get(ValueLayout.JAVA_BYTE, i)); 
        } 
    }

アリーナには、限定共有の2種類があります:

  • Arena::openConfinedによって作成される限定アリーナには、所有者スレッドがあります。通常は、作成に使用されたスレッドです。限定アリーナに割り当てられたメモリー・セグメントには、所有者スレッドしかアクセスできません。限定アリーナを所有者スレッド以外のスレッドによって閉じようとすると、例外が発生します。
  • Arena::openSharedによって作成される共有アリーナには、所有者スレッドはありません。共有アリーナに割り当てられたメモリー・セグメントには、複数のスレッドがアクセスできます。また、どのスレッドでも共有アリーナを閉じることができ、閉じる処理は安全かつ原子性に対応することが保証されます。

オフヒープ・メモリーの割当て

ArenaインタフェースがSegmentAllocatorインタフェースを実装します。これには、オフヒープ・メモリーを割り当て、Javaデータをそれにコピーするメソッドが含まれています。allocateCharArrayの例では、メソッドSegmentAllocator::allocateUtf8Stringをコールします。これは、文字列を、UTF-8でエンコードされたNULLで終了するC文字列に変換し、その結果をメモリー・セグメントに格納します。

    static void allocateCharArray(String s) {
        try (Arena arena = Arena.openConfined()) {

            // Allocate off-heap memory
            MemorySegment nativeText = arena.allocateUtf8String(s);
            // ...

より複雑なネイティブ・データ型(C構造体など)の割当てやアクセスの詳細は、「メモリー・レイアウトおよび構造化アクセス」を参照してください。

オフヒープ・メモリーへのアクセス

次のコードは、nativeTextという名前のMemorySegmentに格納された文字を出力します:

            // Access off-heap memory
            for (int i = 0; i < s.length(); i++ ) {
               System.out.print((char)nativeText.get(ValueLayout.JAVA_BYTE, i)); 
            }

MemorySegmentには、読取りや書込みのために使用できる様々なアクセス・メソッドが含まれます。各アクセス・メソッドは、引数として値レイアウトを取ります。これは、基本データ型(プリミティブなど)に関連付けられたメモリー・レイアウトをモデル化するものです。値レイアウトでは、サイズ、エンディアンすなわちバイト順序、アクセスされるメモリー部分のビット・アラインメント、およびアクセス操作に使用されるJava型がエンコードされます。

たとえば、MemoryLayout.get(ValueLayout.OfByte,long)は引数としてValueLayout.JAVA_BYTEを取ります。この値レイアウトには次の特性があります:

  • Java byteと同じサイズ
  • ビット・アラインメントが8に設定: つまり、メモリー・レイアウトは8ビットの倍数のメモリー・アドレスに格納されます。
  • バイト順序がByteOrder.nativeOrder()に設定: システムは、複数バイト値のバイトを重要な方から重要でない方(ビッグ・エンディアン)または重要でない方から重要な方(リトル・エンディアン)に並べることができます。

外部関数およびメモリーAPIを使用したCライブラリ関数のコール

次の例は、外部関数およびメモリーAPIを使用してstrlenをコールしています:

    static long invokeStrlen(String s) throws Throwable {
        
        try (Arena arena = Arena.openConfined()) {
            
            // 1. Allocate off-heap memory, and
            // 2. Dereference off-heap memory
            MemorySegment nativeString = arena.allocateUtf8String(s);
        
            // 3. Link and call C function
        
            // 3a. Obtain an instance of the native linker
            Linker linker = Linker.nativeLinker();
        
            // 3b. Locate the address of the C function
            SymbolLookup libc = linker.defaultLookup();
            MemorySegment strlen_addr = libc.find("strlen").get();
        
            // 3c. Create a description of the native function signature
            FunctionDescriptor strlen_sig =
                FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS);
            
            // 3d. Create a downcall handle for the C function    
            MethodHandle strlen = linker.downcallHandle(strlen_addr, strlen_sig);

            // 3e. Call the C function directly from Java
            return (long)strlen.invokeExact(nativeString);
        } 
    }

strlen C標準ライブラリ関数の宣言を次に示します:

size_t strlen(const char *s);

これは1つの引数(文字列)を取り、その文字列の長さを返します。Javaアプリケーションからこの関数を呼び出すには、次のステップに従います:

  1. strlen関数の引数のために、オフヒープ・メモリー (Javaランタイム外部のメモリー)を割り当てます。

  2. Java文字列を、割り当てたオフヒープ・メモリーに格納します。

    invokeStrlenの例では、前のステップとこのステップを次の文で実行します:

    MemorySegment nativeString = arena.allocateUtf8String(s);
  3. strlen関数を指すメソッド・ハンドルを作成して呼び出します。この項のトピックでその方法を説明します。

ネイティブ・リンカーのインスタンスの取得

リンカーによって、Javaコードから外部関数へのアクセスと外部関数からJavaコードへのアクセスが提供されます。ネイティブ・リンカーは、Javaランタイムが実行されているプラットフォームのコール規則に準拠したライブラリへのアクセスを提供します。これらのライブラリはネイティブ・ライブラリと呼ばれます。

            Linker linker = Linker.nativeLinker();

C関数のアドレスの確認

ネイティブ・メソッド(strlenなど)をコールするには、ネイティブ関数を指すMethodHandleインスタンスであるダウンコール・メソッド・ハンドルが必要です。このインスタンスには、ネイティブ関数のアドレスが必要です。次の文によって、strlen関数のアドレスが取得されます:

        try (Arena arena = Arena.openConfined()) {    
            SymbolLookup libc = SymbolLookup.libraryLookup("libc.so.6", arena.scope());
            MemorySegment strlen_addr = libc.find("strlen").get();
            // ...
        }

SymbolLookup::libraryLookup(String, SegmentScope)によって、ユーザー指定のネイティブ・ライブラリ内のすべてのシンボルを検索するライブラリ検索が作成されます。これでネイティブ・ライブラリがロードされ、それがセグメント・スコープに関連付けられます。この例で、libc.so.6は、多くのLinuxシステムでのC標準ライブラリのファイル名です。

strlenはC標準ライブラリに含まれるため、ネイティブ・リンカーのデフォルト検索をかわりに使用できます。これは、よく使用される一連のライブラリ(C標準ライブラリを含む)内のシンボルを検索するものです。つまり、システムによって異なるライブラリ・ファイル名を指定する必要がありません:

            // 3a. Obtain an instance of the native linker
            Linker linker = Linker.nativeLinker();
        
            // 3b. Locate the address of the C function
            SymbolLookup stdLib = linker.defaultLookup();
            MemorySegment strlen_addr = stdLib.find("strlen").get();

ノート:

SymbolLookup.loaderLookup()をコールして、System.loadLibrary(String)でロードされるライブラリでシンボルを探します。

C関数シグネチャの説明

ダウンコール・メソッド・ハンドルには、ネイティブ関数のシグネチャの説明も必要です。これはFunctionDescriptorインスタンスによって表されます。関数ディスクリプタは、ネイティブ関数の引数のレイアウトとその戻り値(ある場合)を記述するものです。

関数ディスクリプタ内の各レイアウトはJava型にマップされます。これは、結果のダウンコール・メソッド・ハンドルを呼び出すときに使用する型です。ほとんどの値レイアウトは、Javaプリミティブ型にマップされます。たとえば、ValueLayout.JAVA_INTint値にマップされます。ただし、ValueLayout.ADDRESSはポインタにマップされます。

struct型やunionなどの複合型は、GroupLayoutインタフェースでモデル化されます。これはStructLayoutおよびUnionLayoutのスーパータイプです。C構造体の初期化およびアクセス方法の例は、 「メモリー・レイアウトおよび構造化アクセス」を参照してください。

次によって、strlen関数の関数ディスクリプタが作成されます:

            // 3c. Create a description of the C function signature
            FunctionDescriptor strlen_sig =
                FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS);

FunctionDescriptor::ofメソッドの最初の引数は、ネイティブ関数の戻り値のレイアウトです。ネイティブ・プリミティブ型は、それらの型とサイズが一致する値レイアウトを使用してモデル化されます。つまり、関数ディスクリプタはプラットフォーム固有です。たとえば、size_tは64ビットまたはx64プラットフォームではJAVA_LONGのレイアウトですが、32ビットまたはx86プラットフォームではJAVA_INTのレイアウトです。

FunctionDescriptor::ofの後続の引数は、ネイティブ関数の引数のレイアウトです。この例では、後続の引数はValueLayout.ADDRESSのみです。これは、strlenの唯一の引数(文字列へのポインタ)を表します。

C関数のダウンコール・ハンドルの作成

次の文では、strlen関数のアドレスと関数ディスクリプタを使用して、この関数のダウンコール・メソッド・ハンドルが作成されます。

            // 3d. Create a downcall handle for the C function    
            MethodHandle strlen = linker.downcallHandle(strlen_addr, strlen_sig);  

JavaからのC関数の直接コール

次の文では、strlen関数の引数を含むメモリー・セグメントを使用してこの関数がコールされます:

            // 3e. Call the C function directly from Java
            return (long)strlen.invokeExact(nativeString);

予期される戻り型(この場合はlong)を指定してメソッド・ハンドル呼出しをキャストする必要があります。

アップコール: Javaコードを関数ポインタとして外部関数に渡す

アップコールは、ネイティブ・コードからJavaコードに戻すコールです。アップコール・スタブによってJavaコードを関数ポインタとして外部関数に渡すことができます。

配列の要素をソートする、標準Cライブラリ関数qsortについて考えてみます:

void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

これは4つの引数を取ります:

  • base: ソート対象の配列の最初の要素へのポインタ
  • nbemb: 配列の要素数
  • size: 配列内の各要素のサイズ(バイト)
  • compar: 2つの要素を比較する関数へのポインタ

次の例では、qsort関数をコールしてint配列をソートします。ただし、このメソッドには2つの配列要素を比較する関数へのポインタが必要です。この例では、比較メソッド(Qsort::qsortCompare)を定義し、この比較メソッドを表すメソッド・ハンドルを作成し、このメソッド・ハンドルの関数ポインタを作成します。

import java.lang.foreign.*;
import java.lang.invoke.*;
import java.lang.foreign.ValueLayout.*;

public class InvokeQsort {
        
    class Qsort {
        static int qsortCompare(MemorySegment addr1, MemorySegment addr2) {
            return addr1.get(ValueLayout.JAVA_INT, 0) - addr2.get(ValueLayout.JAVA_INT, 0);
        }
    }
    
    // Obtain instance of native linker
    final static Linker linker = Linker.nativeLinker();
    
    // Create downcall handle for qsort
    final static MethodHandle qsort = linker.downcallHandle(
        linker.defaultLookup().find("qsort").get(),
        FunctionDescriptor.ofVoid(
            ValueLayout.ADDRESS,
            ValueLayout.JAVA_LONG,
            ValueLayout.JAVA_LONG,
            ValueLayout.ADDRESS));

    // A Java description of a C function implemented by a Java method
    final static FunctionDescriptor qsortCompareDesc = FunctionDescriptor.of(
        ValueLayout.JAVA_INT,
        ValueLayout.ADDRESS.asUnbounded(),
        ValueLayout.ADDRESS.asUnbounded());

    // Create method handle for qsortCompare
    final static MethodHandle compareHandle;
    static {
        try {   
            compareHandle = MethodHandles.lookup().findStatic(
                InvokeQsort.Qsort.class,
                "qsortCompare",
                qsortCompareDesc.toMethodType());
        } catch (Exception e) {
            throw new AssertionError(
                "Problem creating method handle compareHandle", e);
        }
    }    
    
    static int[] qsortTest(int[] unsortedArray) throws Throwable {
        
        int[] sorted = null;        
        
        try (Arena arena = Arena.openConfined()) {                    
        
            // Allocate off-heap memory and store unsortedArray in it                
            MemorySegment array = arena.allocateArray(
                                          ValueLayout.JAVA_INT,
                                          unsortedArray);        
        
            // Create function pointer for qsortCompare
            MemorySegment compareFunc = linker.upcallStub(
                compareHandle,
                qsortCompareDesc,
                arena.scope());
                    
            // Call qsort        
            qsort.invoke(array, (long)unsortedArray.length,
                ValueLayout.JAVA_INT.byteSize(), compareFunc);
            
            // Access off-heap memory
            sorted = array.toArray(ValueLayout.JAVA_INT);                
        }
        return sorted;
    }        
    
    public static void main(String[] args) {
        try { 
            int[] sortedArray = InvokeQsort.qsortTest(
                new int[] { 0, 9, 3, 4, 6, 5, 1, 8, 2, 7 });
            for (int num : sortedArray) {
                System.out.print(num + " ");
            }
            System.out.println();
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

次のクラスによって、2つの要素(この場合は2つのint値)を比較するJavaメソッドが定義されます:

    class Qsort {
        static int qsortCompare(MemorySegment addr1, MemorySegment addr2) {
            return addr1.get(ValueLayout.JAVA_INT, 0) - addr2.get(ValueLayout.JAVA_INT, 0);
        }
    }

このメソッドでは、int値はMemorySegmentオブジェクトによって表されます。メモリー・セグメントを使用すると、メモリーの連続した領域にアクセスできます。メモリー・セグメントの値を取得するには、そのgetメソッドの1つをコールします。この例では、get(ValueLayout.OfInt, long)をコールします。ここで、2番目の引数は、メモリー・アドレスの場所を基準としたオフセット(バイト単位)です。この例のメモリー・セグメントには1つの値しか格納されないため、2番目の引数は0です。

次の文によって、qsort関数のダウンコール・メソッド・ハンドルが作成されます:

    // Obtain instance of native linker
    final static Linker linker = Linker.nativeLinker();
    
    // Create downcall handle for qsort
    final static MethodHandle qsort = linker.downcallHandle(
        linker.defaultLookup().find("qsort").get(),
        FunctionDescriptor.ofVoid(
            ValueLayout.ADDRESS,
            ValueLayout.JAVA_LONG,
            ValueLayout.JAVA_LONG,
            ValueLayout.ADDRESS));

次の文によって、比較メソッドQsort::qsortCompareを表すメソッド・ハンドルが作成されます:

    // A Java description of a C function implemented by a Java method
    final static FunctionDescriptor qsortCompareDesc = FunctionDescriptor.of(
        ValueLayout.JAVA_INT,
        ValueLayout.ADDRESS.asUnbounded(),
        ValueLayout.ADDRESS.asUnbounded());
    // Create method handle for qsortCompare
    final static MethodHandle compareHandle;
    static {
        try {   
            compareHandle = MethodHandles.lookup().findStatic(
                InvokeQsort.Qsort.class,
                "qsortCompare",
                qsortCompareDesc.toMethodType());
        } catch (Exception e) {
            throw new AssertionError(
                "Problem creating method handle compareHandle", e);
        }
    }

ノート:

ValueLayout.OfAddress::asUnbounded制限されたメソッドです。使用方法が正しくない場合、JVMがクラッシュしたり、エラーが表示されずにメモリーが破損したりすることがあります。詳細は、「制限されたメソッド」を参照してください。

MethodHandles.Lookup::findStaticメソッドは、静的メソッドのメソッド・ハンドルを作成します。これは次の3つの引数を取ります。

  • メソッドのクラス
  • メソッドの名前
  • メソッドの型: MethodType::methodTypeの最初の引数は、メソッドの戻り値の型です。残りは、メソッドの引数の型です。

次の文によって、オフヒープ・メモリーが割り当てられ、ソート対象のint配列がその中に格納されます:

        try (Arena arena = Arena.openConfined()) {                    
        
            // Allocate off-heap memory and store unsortedArray in it                
            MemorySegment array = arena.allocateArray(
                                          ValueLayout.JAVA_INT,
                                          unsortedArray);

次の文によって、メソッド・ハンドルcompareHandleの関数ポインタが作成されます:

            // Create function pointer for qsortCompare
            MemorySegment compareFunc = linker.upcallStub(
                compareHandle,
                qsortCompareDesc,
                arena.scope());

Linker::upcallStubメソッドは3つの引数を取ります:

  • 関数ポインタを作成するメソッド・ハンドル
  • 関数ポインタの関数ディスクリプタ。この例では、FunctionDescriptor.ofの引数はQsort::qsortCompareの戻り値の型と引数に対応しています
  • 関数ポインタに関連付けるスコープ

次の文によって、qsort関数がコールされます:

            // Call qsort        
            qsort.invoke(array, (long)unsortedArray.length,
                ValueLayout.JAVA_INT.byteSize(), compareFunc);

この例で、MethodHandle::invokeの引数は、標準Cライブラリqsort関数の引数に対応します。

最後に、次の文によって、ソートされた配列値がオフヒープ・メモリーからオンヒープ・メモリーにコピーされます:

            // Access off-heap memory
            sorted = array.toArray(ValueLayout.JAVA_INT);;   

メモリー・レイアウトおよび構造化アクセス

基本の操作のみを使用して構造化データにアクセスすると、保守が困難で理解しにくいコードになる可能性があります。かわりに、メモリー・レイアウトを使用すると、より複雑なネイティブ・データ型(C構造体など)を効率よく初期化してアクセスすることができます。

たとえば、次のC宣言について考えてみます。これはPoint構造体の配列を定義し、各Point構造体にはPoint.xPoint.yという2つのメンバーが含まれます:

struct Point {
   int x;
   int y;
} pts[10];

このようなネイティブ配列は、次のように初期化できます:

        try (Arena arena = Arena.openConfined()) {

            MemorySegment segment =
                arena.allocate((long)(2 * 4 * 10), 1);

            for (int i = 0; i < 10; i++) {
                segment.setAtIndex(ValueLayout.JAVA_INT, (i * 2),     i); // x
                segment.setAtIndex(ValueLayout.JAVA_INT, (i * 2) + 1, i); // y
            }
            // ...
        }

Arena::allocateメソッドのコールの最初の引数によって、配列に必要なバイト数が計算されます。MemorySegment::setAtIndexメソッドのコールの引数によって、Point構造体の各メンバーに書き込むためのメモリー・アドレスのオフセットが計算されます。これらの計算を回避するには、メモリー・レイアウトを使用できます。

Point構造体の配列を表すために、次の例ではシーケンス・メモリー・レイアウトを使用します:

        try (Arena arena = Arena.openConfined()) {
            
            SequenceLayout ptsLayout
                = MemoryLayout.sequenceLayout(10,
                    MemoryLayout.structLayout(
                        ValueLayout.JAVA_INT.withName("x"),
                        ValueLayout.JAVA_INT.withName("y")));

            VarHandle xHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("x"));
            VarHandle yHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("y"));            

            MemorySegment segment = arena.allocate(ptsLayout);
            
            for (int i = 0; i < ptsLayout.elementCount(); i++) {
                xHandle.set(segment, (long) i, i);
                yHandle.set(segment, (long) i, i);
            }
            // ...
        }

最初の文によって、SequenceLayoutオブジェクトで表されるシーケンス・メモリー・レイアウトが作成されます。これには、StructLayoutオブジェクトで表される10個の構造体レイアウトのシーケンスが含まれます。メソッドMemoryLayout::structLayoutは、StructLayoutオブジェクトを返します。各構造体レイアウトには、xyという名前の2つのJAVA_INT値レイアウトが含まれます:

            SequenceLayout ptsLayout
                = MemoryLayout.sequenceLayout(10,
                    MemoryLayout.structLayout(
                        ValueLayout.JAVA_INT.withName("x"),
                        ValueLayout.JAVA_INT.withName("y")));

事前定義値ValueLayout.JAVA_INTには、Java int値に必要なバイト数に関する情報が含まれます。

次の文によって、メモリー・アドレス・オフセットを取得する2つのメモリー・アクセスVarHandleが作成されます。VarHandleは、動的な強い型指定の参照であり、変数を参照するか、パラメータによって定義された変数ファミリ(静的フィールド、非静的フィールド、配列要素、オフヒープ・データ構造体のコンポーネント)を参照します。

            VarHandle xHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("x"));
            VarHandle yHandle
                = ptsLayout.varHandle(PathElement.sequenceElement(),
                    PathElement.groupElement("y")); 

メソッドPathElement.sequenceElement()は、シーケンス・レイアウトからメモリー・レイアウトを取得します。この例では、構造体レイアウトの1つをptsLayoutから取得します。メソッド・コールPathElement.groupElement("x")は、xという名前のメモリー・レイアウトを取得します。名前が付いたメモリー・レイアウトを作成するにはwithName(String)メソッドを使用します。

for文によって、VarHandle::setがコールされ、MemorySegment::setAtIndexのようにメモリーにアクセスします。この例では、値(3番目の引数)がメモリー・セグメント(最初の引数)のインデックス(2番目の引数)に設定されます。VarHandle xHandleおよびyHandleは、Point構造体のサイズ(8バイト)とそのintメンバーのサイズ(4バイト)を認識しています。つまり、setAtIndexメソッドのように、配列の要素またはメモリー・アドレス・オフセットに必要なバイト数を計算する必要はありません。

            MemorySegment segment = arena.allocate(ptsLayout);
            
            for (int i = 0; i < ptsLayout.elementCount(); i++) {
                xHandle.set(segment, (long) i, i);
                yHandle.set(segment, (long) i, i);
            }

制限されるメソッド

外部関数およびメモリー(FFM) APIの一部のメソッドは安全ではないため、制限されます。制限されたメソッドは、正しく使用しないと、JVMをクラッシュさせ、メモリーの破損につながる可能性があります(エラーは表示されません)。

実行するアプリケーションで、次に示す制限されたメソッドのいずれかが呼び出されると、Javaランタイムによって警告メッセージが出力されます:モジュールMのコードで、これらの制限されたメソッドまたは安全でないメソッドを警告が表示されずに使用できるようにするには、--enable-native-access=Mコマンドライン・オプションを指定します。複数のモジュールはカンマ区切りリストで指定します。クラス・パス上のすべてのコードで警告なしの使用を可能にするには、--enable-native-access=ALL-UNNAMEDオプションを指定します。

表13-1 FFM APIの制限されたメソッド

メソッド 説明

Linker::nativeLinker()

SymbolLookup::libraryLookup(String, SegmentScope)

SymbolLookup::libraryLookup(Path, SegmentScope)

これらのメソッドは、本質的に安全ではないダウンコール・メソッド・ハンドルを作成するために必要です。通常、外部ライブラリのシンボルには、十分なシグネチャ情報(外部関数パラメータの個数や型など)が含まれていないため、リンカーが実行時にリンク要求を検証できません。クライアントがやり取りするダウンコール・メソッド・ハンドルが、無効なリンク要求によって取得されていたとき(たとえば、指定した関数ディスクリプタの引数レイアウトが多すぎた場合など)、このようなやり取りの結果は未指定となり、JVMがクラッシュする可能性があります。

JVMのクラッシュはダウンコールやアップコールで発生する可能性があります。これは、リンカーに渡されるネイティブ関数タイプとFunctionDescriptorが不一致なためです。さらに、コールされる関数がアンロードされると、ダウンコールとアップコールがクラッシュする可能性があります。ダウンコールでは、これは、関数を含むライブラリがアンロードされると発生します。アップコールでは、これはアップコール・スタブが割当て解除されると発生します。たとえば、関連付けられたスコープが閉じられるためです。

MemorySegment::ofAddress(long, long)

MemorySegment::ofAddress(long, long, SegmentScope)

MemorySegment::ofAddress(long, long, SegmentScope, Runnable)

VaList::ofAddress(long, SegmentScope)

場合によっては、ネイティブ・コードから取得したRAWメモリー・アドレスを、完全な空間、時間および制限の境界を備えたメモリー・セグメントに変換する必要があります。こうすることで、メモリー・セグメントに直接アクセスできます。このために、クライアントは、これらのメソッドの1つをコールして、セグメント・サイズとセグメント・スコープを指定することでネイティブ・セグメントをメモリー・アドレスから取得できますが、安全ではありません。これは制限された操作です。セグメント・サイズが正しくない場合、メモリー・セグメントをアクセスしようとしてJVMがクラッシュする可能性があるためです。たとえば、RAWメモリー・アドレスは10バイト長のメモリー領域に関連付けられている場合がありますが、クライアントは領域のサイズを過大評価して、100バイト長のメモリー・セグメントを作成することがあります。このために、後で領域の境界外のメモリーにアクセスが試行される可能性があり、JVMクラッシュが発生したり、さらにはエラーが表示されずにメモリーが破損したりすることさえあります。
ValueLayout.OfAddress::asUnbounded() FFM APIが、ネイティブ関数との対話型操作やValueLayout.ADDRESSレイアウトによるメモリー・セグメントの読取りなどでポインタを取得するとき、返されるメモリー・セグメントのサイズを認識しません。この理由から、このようなサイズはゼロに設定されます。これは安全ですが、クライアントは、これ以降のアクセス操作で、返されたメモリー・セグメントを使用することはできません。このためには、ValueLayout.OfAddress::asUnbounded()をコールしてバインドされていないアドレス・レイアウトを取得します。これは、Long.MAX_VALUEなど最大サイズのメモリー・セグメントにマップされ、クライアントがアクセス操作に使用できます。「アップコール: Javaコードを関数ポインタとして外部関数に渡す」で説明されているInvokeQsortの例は、バインドされていないアドレス・レイアウトを使用しています。これにより、compareFuncアップコールで、渡される2つのポインタの内容にすぐにアクセスして比較できます。

jextractを使用したネイティブ関数のコール

jextractツールは、ネイティブ・ライブラリのヘッダー・ファイルからJavaバインディングを機械的に生成します。このツールが生成するバインディングは、外部関数およびメモリー(FFM) APIに依存します。このツールを使用すると、呼び出す関数のダウンコール・ハンドルおよびアップコール・ハンドルを作成する必要はありません。jextractツールによって、そのためのコードが生成されます。

次のサイトからツールを入手します:

https://jdk.java.net/jextract/

jextractのソース・コードは次のサイトで入手します:

https://github.com/openjdk/jextract

このサイトには、jextractをコンパイルおよび実行する方法のステップ、その他のドキュメントおよびサンプルも含まれています。

JavaアプリケーションでのPythonスクリプトの実行

次のステップでは、Pythonヘッダー・ファイルPython.hからJavaバインディングを生成し、生成されたコードを使用してJavaアプリケーションでPythonスクリプトを実行する方法を示します。このPythonスクリプトは、Java文字列の長さを出力します。

  1. 次のコマンドを実行して、Python.hのJavaバインディングを生成します:
    jextract -l <absolute path of Python shared library> \
      --output classes \
      -I <directory containing Python header files> \
      -t org.python <absolute path of Python.h>

    たとえば:

    jextract -l /lib64/libpython3.6m.so.1.0 \
      --output classes \
      -I /usr/include/python3.6m \
      -t org.python /usr/include/python3.6m/Python.h 

    ノート:

    • Linuxシステムで、Python共有ライブラリのファイル名を取得するには、次のコマンドを実行します。この例では、システムにPython 3がインストールされていることを前提としています。

      ldd $(which python3)

      このコマンドを実行すると、次のような出力が表示されます:

              linux-vdso.so.1 =>  (0x00007ffdb4bd5000)
              libpython3.6m.so.1.0 => /lib64/libpython3.6m.so.1.0 (0x00007fb0386a7000)
              libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fb03848b000)
              libdl.so.2 => /lib64/libdl.so.2 (0x00007fb038287000)
              libutil.so.1 => /lib64/libutil.so.1 (0x00007fb038084000)
              libm.so.6 => /lib64/libm.so.6 (0x00007fb037d82000)
              libc.so.6 => /lib64/libc.so.6 (0x00007fb0379b4000)
              /lib64/ld-linux-x86-64.so.2 (0x00007fb038bce000)
    • Linuxシステムで、Python.hまたはPythonヘッダー・ファイルを含むディレクトリが見つからない場合は、python-develパッケージをインストールする必要がある場合があります。

    • jextractツールによって作成されるクラスおよびメソッドを調べる場合は、--sourceオプションを指定してコマンドを実行します。たとえば、次のコマンドによって、Python.hのJavaバインディングのソース・ファイルが生成されます:
      jextract --source \
        --output src \
        -I <directory containing Python header files> \
        -t org.python <absolute path of Python.h>
  2. classesと同じディレクトリ(Python Javaバインディングが含まれる)に、次のファイルPythonMain.javaを作成します:
    import java.lang.foreign.Arena;
    import java.lang.foreign.MemorySegment;
    import static java.lang.foreign.MemorySegment.NULL;
    import static org.python.Python_h.*;
    
    public class PythonMain {
        
        public static void main(String[] args) {
            String myString = "Hello world!";
            String script = """
                         string = "%s"
                         print(string, ': ', len(string), sep='')
                         """.formatted(myString).stripIndent();
            Py_Initialize();
            
            try (Arena arena = Arena.openConfined()) {
                MemorySegment nativeString = arena.allocateUtf8String(script);
                PyRun_SimpleStringFlags(
                    nativeString,
                    NULL);
                Py_Finalize();
            }
            Py_Exit(0);
        }
    }
  3. 次のコマンドを使用して、PythonMain.javaをコンパイルします:
    javac --enable-preview -source 20 -cp classes PythonMain.java
  4. 次のコマンドを使用して、PythonMainを実行します:
    java --enable-preview -cp classes:. --enable-native-access=ALL-UNNAMED PythonMain

Javaアプリケーションからのqsort関数のコール

前述のように、qsortは、2つの要素を比較する関数へのポインタを必要とするCライブラリ関数です。次のステップでは、jextractを使用してC標準ライブラリのJavaバインディングを作成し、qsortで必要な比較関数のアップコール・ハンドルを作成し、qsort関数をコールします。

  1. 次のコマンドを実行して、C標準ライブラリのヘッダー・ファイルであるstdlib.hのJavaバインディングを作成します:
    jextract --output classes -t org.unix <absolute path to stdlib.h>

    たとえば:

    jextract --output classes -t org.unix /usr/include/stdlib.h

    stdlib.hに対して生成されるJavaバインディングに含まれるのは、stdlib_hという名前のJavaクラス(qsort(MemorySegment, long, long, MemorySegment)という名前のJavaメソッドが含まれる)と、__compar_fn_tという名前のJavaインタフェース(allocateという名前のメソッドが含まれる)です。このメソッドによって、qsort関数で必要な比較関数の関数ポインタが作成されます。jextractによって生成されるJavaバインディングのソース・コードを調べるには、--sourceオプションを指定してツールを実行します:

    jextract --source --output src -t org.unix <absolute path to stdlib.h>
  2. stdlib.hのJavaバインディングを生成したディレクトリと同じディレクトリに、次のJavaソース・ファイルQsortMain.javaを作成します:
    import static org.unix.__compar_fn_t.*;
    import static org.unix.stdlib_h.*;
    import java.lang.foreign.*;
    import java.lang.invoke.*;
    
    public class QsortMain {
        
        public static void main(String[] args) {
            
            int[] unsortedArray = new int[] { 0, 9, 3, 4, 6, 5, 1, 8, 2, 7 };
    
            try (Arena a = Arena.openConfined()) {
                
                // Allocate off-heap memory and store unsortedArray in it
                MemorySegment array = a.allocateArray(
                    ValueLayout.JAVA_INT,
                    unsortedArray);            
    
                // Create upcall for comparison function
                MemorySegment comparFunc = allocate(
                    (addr1, addr2) ->
                        Integer.compare(
                            addr1.get(ValueLayout.JAVA_INT, 0),
                            addr2.get(ValueLayout.JAVA_INT, 0)),
                        a.scope());
               
                // Call qsort
                qsort(array, (long) unsortedArray.length,
                    ValueLayout.JAVA_INT.byteSize(), comparFunc);      
    
                // Deference off-heap memory
                int[] sortedArray = array.toArray(ValueLayout.JAVA_INT);
    
                for (int num : sortedArray) {
                    System.out.print(num + " ");
                }
                System.out.println();        
            } 
        }
    }

    次の文によって、ラムダ式からアップコールcomparFuncが作成されます:

                // Create upcall for comparison function
                MemorySegment comparFunc = allocate(
                    (addr1, addr2) ->
                        Integer.compare(
                            addr1.get(ValueLayout.JAVA_INT, 0),
                            addr2.get(ValueLayout.JAVA_INT, 0)),
                        arena.scope());

    したがって、アップコール: Javaコードを関数ポインタとして外部関数に渡すで説明されているように、比較関数のメソッド・ハンドルを作成する必要はありません。

  3. 次のコマンドを使用して、QsortMain.javaをコンパイルします:
    javac --enable-preview -source 20 -cp classes QsortMain.java
  4. 次のコマンドを使用して、QsortMainを実行します:
    java --enable-preview -cp classes:. --enable-native-access=ALL-UNNAMED QsortMain