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

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

ノート:

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

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

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

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

C標準ライブラリの関数strlenについて考えます:

size_t strlen(const char *s);

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

  1. strlen関数の引数のために、オフヒープ・メモリー (Javaランタイム外部のメモリー)を割り当てます。オフヒープ・メモリーの割当てを参照してください。
  2. Java文字列を、割り当てたオフヒープ・メモリーに格納します。オフヒープ・メモリーの間接参照を参照してください。

    ノート:

    メソッドによっては、これらの2つのステップを1つのメソッド・コールで実行できます。オフヒープ・メモリーの割当てと移入を行うメソッドを参照してください。
  3. strlen関数を指すメソッド・ハンドルを作成して呼び出します。C関数のリンクおよびコールを参照してください。

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

    static long invokeStrlen(String s) throws Throwable {
        
        try (MemorySession session = MemorySession.openConfined()) {
            
            // 1. Allocate off-heap memory, and
            // 2. Dereference off-heap memory
            MemorySegment nativeString = session.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 stdLib = linker.defaultLookup();
            MemorySegment strlen_addr = stdLib.lookup("strlen").get();
        
            // 3c. Create a description of the C 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.invoke(nativeString);
        } 
    }

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

オフヒープ・データは、Javaランタイム外部のメモリーに格納されるデータであるため、ガベージ・コレクションの対象にはなりません。オフヒープ・データはオフヒープ・メモリーに格納され、MemorySegmentオブジェクトによって表されます。JavaアプリケーションからC関数を呼び出すには、その引数がオフヒープ・メモリー内にある必要があります。

次の例では、Java文字列の内容を格納するメモリー・セグメントをオフヒープに作成します:

String s = ...
MemorySegment nativeString =
    MemorySegment.allocateNative(s.length()+1, MemorySession.global());

この例では、サイズs.length() + 1のオフヒープ・メモリーのブロックを割り当てます。これは、Java文字列の内容と末尾の終了文字を保持するのに十分な大きさです。

すべてのメモリー・セグメントはメモリー・セッションに関連付けられ、これによって、いつメモリー・セグメントが有効か(アクセスできるか)を判別します。メモリーセッションはMemorySessionオブジェクトによって表されます。前述の例では、メモリー・セグメントはグローバル・メモリー・セッション(クローズできないメモリー・セッション)に関連付けられています。その結果、オフヒープ・メモリーはJVMプロセスの終了時にのみ割当て解除されます。クローズできるメモリー・セッションが必要な場合は、限定メモリー・セッションを使用できます:

String s = ...  
try (MemorySession session = MemorySession.openConfined()) {
    MemorySegment.allocateNative(s.length()+1, session);
    // ...
} // nativeString is deallocated here

限定メモリー・セッションとは、現在のスレッドのみがアクセスできるセッションです。この例では、try-with-resources文で限定メモリー・セッションを作成します。try-with-resourcesブロックの最後で、限定メモリーがクローズされ、それに関連付けられたすべてのオフヒープ・メモリーが解放されます。

オフヒープ・メモリーの間接参照

空のメモリー・セグメントnativeStringを作成した後、次のステップとして、Java文字列の文字をそのセグメントにコピーします:

for(int i = 0; i < s.length(); i++) {
    nativeString.set(ValueLayout.JAVA_BYTE, i, (byte)s.charAt(i));
}

// Add the string terminator at the end
nativeString.set(ValueLayout.JAVA_BYTE, s.length(),(byte)0);

MemorySegment::setメソッドの最初の引数はValueLayout.OfByte型です。値レイアウトは、プリミティブなど基本データ型の値に関連付けられたメモリー・レイアウトをモデル化します。値レイアウトによって、サイズ、エンディアン、間接参照するメモリーの部分の配置、および間接参照操作に使用するJava型がエンコードされます。この例でValueLayout.JAVA_BYTEはサイズが1バイトでネイティブのエンディアンですが、この例の読取りは1バイトのみであるため関係ありません。

このレイアウトを使用するとき、APIは、クライアントがJavaの間接参照値をプリミティブ型byteとしてエンコードすると想定します。MemoryLayout::setメソッドの2番目の引数は、byte値を書き込むメモリー・セグメントのアドレスを相対的に示すインデックスです。

オフヒープ・メモリーには、さらに複雑なデータ型を割り当てて移入することができます。メモリー・レイアウトおよび構造化アクセスを参照してください。

オフヒープ・メモリーの割当てと移入を行うメソッド

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

    static long invokeStrlen(String s) throws Throwable {
        
        try (MemorySession session = MemorySession.openConfined()) {
            
            // 1. Allocate off-heap memory, and
            // 2. Dereference off-heap memory
            MemorySegment nativeString = session.allocateUtf8String(s);
            // ...
        }
    }

C関数のリンクおよびコール

C標準ライブラリのstrlen関数などネイティブ関数のコールには、次のステップが含まれます:

  1. ネイティブ・リンカーのインスタンスの取得
  2. C関数のアドレスの確認
  3. C関数シグネチャの説明の作成
  4. C関数のダウンコール・ハンドルの作成
  5. JavaからのC関数の直接コール
ネイティブ・リンカーのインスタンスの取得

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

            Linker linker = Linker.nativeLinker();
C関数のアドレスの確認

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

            Linker linker = Linker.nativeLinker();
            SymbolLookup libc = SymbolLookup.libraryLookup("libc.so.6", session);
            MemorySegment strlen_addr = libc.lookup("strlen").get();

SymbolLookup::libraryLookup(String, MemorySession)によって、ユーザー指定のネイティブ・ライブラリ内のすべてのシンボルを検索するライブラリ検索が作成されます。これによってネイティブ・ライブラリがロードされて、MemorySessionオブジェクトに関連付けられます。この例で、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.lookup("strlen").get();
C関数シグネチャの説明の作成

ダウンコール・メソッド・ハンドルには、ネイティブ関数のシグネチャの説明も必要です。これはFunctionDescriptorインスタンスによって表されます。関数ディスクリプタは、ネイティブ関数の引数のレイアウトとその戻り値(ある場合)を記述するものです。次によって、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です。すべてのポインタの型はこの値レイアウトを使用してモデル化されます。ここに示されていないstruct型は、構造体レイアウトを使用してモデル化されます。メモリー・レイアウトおよび構造化アクセスを参照してください。

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

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

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

ダウンコール・メソッド・ハンドルには、MethodTypeが関連付けられています。これは、メソッド・ハンドルによって受け入れられる引数と返される戻り型を表します。この例では、strlenに関連付けられたMethodTypeには、1つのパラメータAddressableとその戻り型はlongが含まれます。MethodHandle::typeをコールしてメソッド・ハンドルのMethodTypeを取得します。メソッドの型を導出する方法の詳細は、JavaDoc API仕様のLinkerインタフェースでダウンコール・メソッド・ハンドル を参照してください。

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

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

            // 3e. Call the C function directly from Java
            return (long)strlen.invoke(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)を定義し、この比較メソッドを表すメソッド・ハンドルを作成し、このメソッド・ハンドルの関数ポインタを作成します。

    class Qsort {
        static int qsortCompare(MemoryAddress addr1, MemoryAddress addr2) {
            return Integer.compare(
                addr1.get(ValueLayout.JAVA_INT, 0),
                addr2.get(ValueLayout.JAVA_INT, 0));
        }
    }
    
    static int[] qsortTest (int[] unsortedArray) throws Throwable {
        
        int[] sortedArray;
    
        try (MemorySession session = MemorySession.openConfined()) {
            
            // Allocate off-heap memory and store unsortedArray in it
            MemorySegment array = session.allocateArray(
                ValueLayout.JAVA_INT,
                unsortedArray);            
            
            // Obtain instance of native linker
            Linker linker = Linker.nativeLinker();
            
            // Create downcall handle for qsort
            MethodHandle qsort = linker.downcallHandle(
                linker.defaultLookup().lookup("qsort").get(),
                FunctionDescriptor.ofVoid(
                    ValueLayout.ADDRESS,
                    ValueLayout.JAVA_LONG,
                    ValueLayout.JAVA_LONG,
                    ValueLayout.ADDRESS)
            );  
            
            // Create method handle for qsortCompare
            FunctionDescriptor compareFunc_sig = FunctionDescriptor.of(
                ValueLayout.JAVA_INT,
                ValueLayout.ADDRESS,
                ValueLayout.ADDRESS);

            MethodHandle comparHandle = MethodHandles.lookup().findStatic(
                Qsort.class, "qsortCompare", Linker.upcallType(compareFunc_sig));

            // Create function pointer for qsortCompare                    
            MemorySegment comparFunc = linker.upcallStub(comparHandle,
                compareFunc_sig, session);            
           
            // Call qsort
            qsort.invoke(array, (long) unsortedArray.length, 4L, comparFunc);

            // Dereference off-heap memory
            sortedArray = array.toArray(ValueLayout.JAVA_INT);
        }
        return sortedArray;    
    }

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

            // Allocate off-heap memory and store unsortedArray in it
            MemorySegment array = session.allocateArray(
                ValueLayout.JAVA_INT,
                unsortedArray);  

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

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

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

    class Qsort {
        static int qsortCompare(MemoryAddress addr1, MemoryAddress addr2) {
            return Integer.compare(
                addr1.get(ValueLayout.JAVA_INT, 0),
                addr2.get(ValueLayout.JAVA_INT, 0));
        }
    }

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

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

            // Create method handle for qsortCompare
            FunctionDescriptor compareFunc_sig = FunctionDescriptor.of(
                ValueLayout.JAVA_INT,
                ValueLayout.ADDRESS,
                ValueLayout.ADDRESS);

            MethodHandle comparHandle = MethodHandles.lookup().findStatic(
                Qsort.class, "qsortCompare", Linker.upcallType(compareFunc_sig));

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

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

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

            // Create function pointer for qsortCompare                    
            MemorySegment comparFunc = linker.upcallStub(comparHandle,
                compareFunc_sig, session);    

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

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

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

            // Call qsort
            qsort.invoke(array, (long) unsortedArray.length, 4L, comparFunc);

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

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

            // Dereference off-heap memory
            sortedArray = array.toArray(ValueLayout.JAVA_INT);

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

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

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

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

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

        try (MemorySession session = MemorySession.openConfined()) {

            MemorySegment segment =
                MemorySegment.allocateNative(2 * 4 * 10, session);

            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
            }
            // ...
        }

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

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

        try (MemorySession session = MemorySession.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 =
                MemorySegment.allocateNative(ptsLayout, session);
            
            for (int i = 0; i < ptsLayout.elementCount(); i++) {
                xHandle.set(segment, (long) i, i);
                yHandle.set(segment, (long) i, i);
            }
            // ...
        }

最初の文によって、SequenceLayoutオブジェクトで表されるシーケンス・メモリー・レイアウトが作成されます。これには、GroupLayoutオブジェクトで表される10個の構造体レイアウトのシーケンスが含まれます。メソッドMemoryLayout::structLayoutは、GroupLayoutオブジェクトを返します。各構造体レイアウトには、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のようにメモリーを間接参照します。この例では、値(2番目の引数)がメモリー・セグメント(最初の引数)のインデックス(3番目の引数)に設定されます。VarHandle xHandleおよびyHandleは、Point構造体のサイズ(8バイト)とそのintメンバーのサイズ(4バイト)を認識しています。つまり、setAtIndexメソッドのように、配列の要素またはメモリー・アドレス・オフセットに必要なバイト数を計算する必要はありません。

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

制限されるメソッド

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

実行するアプリケーションで、次に示す制限されたメソッドのいずれかが呼び出されると、Javaランタイムによって警告メッセージが出力されます:このメッセージを抑制するには、--enable-native-access=ALL-UNNAMEDコマンドライン・オプションを追加します。

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

    JVMはアップコールでクラッシュする可能性があります。アップコールは、通常、ダウンコール・メソッド・ハンドル呼出しのコンテキストで呼び出されるためです。

  • MemoryAddressのすべての間接参照メソッド: メモリー・アドレスには時間的または空間的な境界がないため、ランタイムにはメモリー間接参照操作の正確性をチェックする方法がありません。
  • MemorySegment::ofAddressおよびVaList::ofAddress: 場合によっては、ネイティブ・コードから取得したメモリー・アドレスを、完全な空間境界、時間境界および限定境界のすべてを備えたメモリー・セグメントに変換する必要があります。このために、クライアントはセグメント・サイズとセグメント・セッションを指定して、メモリー・アドレスからネイティブ・セグメントを取得できますが、安全ではありません。これは制限された操作です。たとえば、セグメント・サイズが正しくない場合、メモリー・セグメントを間接参照しようとしてJVMがクラッシュする可能性があるためです。

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

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

    ノート:

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

      ldd $(which python3)
    • 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.MemoryAddress;
    import java.lang.foreign.MemorySegment;
    import java.lang.foreign.MemorySession;
    import static org.python.Python_h.*;
    import org.python.*;
    
    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 (MemorySession session = MemorySession.openConfined()) {
                MemorySegment nativeString = session.allocateUtf8String(script);
                PyRun_SimpleStringFlags(
                    nativeString,
                    MemoryAddress.NULL);
                Py_Finalize();
            }
            Py_Exit(0);
        }
    }
  3. 次のコマンドを使用して、PythonMain.javaをコンパイルします:
    javac --enable-preview -source 19 \
      -classpath classes \
      PythonMain.java
  4. 次のコマンドを使用して、PythonMainを実行します:
    java -cp classes:. -Djava.library.path=<location of Python shared library> 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>

    stdlib.hに対して生成されるJavaバインディングに含まれるのは、stdlib_hという名前のJavaクラス(qsort(Addressable, long, long, Addressable)という名前の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 (MemorySession session = MemorySession.openConfined()) {
                
                // Allocate off-heap memory and store unsortedArray in it
                MemorySegment array = session.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)),
                        session);
               
                // Call qsort
                qsort(array, (long) unsortedArray.length, 4L, comparFunc);      
    
                // Dereference 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)),
                        session);

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

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