モジュール java.base
パッケージ java.lang.foreign

インタフェースLinker


public sealed interface Linker
リンカーは、Javaコードから外部関数にアクセスでき、外部関数からJavaコードにアクセスできます。

外部関数は、通常、オンデマンドでロードできるライブラリに存在します。 各ライブラリは、特定のABI (アプリケーション・バイナリ・インタフェース)に準拠しています。 ABIは、ライブラリが構築されたコンパイラ、OS、およびプロセッサに関連付けられた呼び出し規則とデータ型のセットです。 たとえば、Linux/x64上のCコンパイラは通常、SystemV ABIに準拠するライブラリを構築します。

リンカーは、特定のABIで使用される呼び出し規則とデータ型について詳細に把握しています。 リンカーは、そのABIに準拠するすべてのライブラリについて、JVM内で実行されているJavaコードとライブラリ内の外部関数を仲介できます。 特に次の点が重要です。

  • リンカーを使用すると、downcall method handlesRESTRICTEDを介して、Javaコードを外部関数とリンクできます
  • リンカーを使用すると、外部関数はupcall stubsRESTRICTEDの生成を介してJavaメソッド・ハンドルをコールできます。
リンカーは、ABIで使用されるデータ型に関連付けられている「標準レイアウト」を検索する方法を提供します。 たとえば、C ABIを実装するリンカーは、C size_tタイプの正規のレイアウトを提供することを選択できます。 64ビット・プラットフォームでは、この正規レイアウトはValueLayout.JAVA_LONGと等しい場合があります。 リンカーでサポートされる正規レイアウトは、canonicalLayouts()メソッドを介して公開されます。このメソッドは、タイプ名から正規レイアウトへのマップを返します。

さらに、リンカーはABIに準拠するライブラリ内の外部関数を検索する方法も提供します。 各リンカーは、ABIに関連付けられたOSとプロセッサの組み合わせで一般的に使用されるライブラリ・セットを選択します。 たとえば、Linux/x64のリンカーは2つのライブラリを選択できます: libclibm これらのライブラリの関数は、「シンボル・ルックアップ」を介して公開されます。

ネイティブ関数の呼び出し

「ネイティブ・リンカー」を使用すると、Cライブラリ (ネイティブ関数)に定義されている関数に対してリンクできます。 Javaから、標準Cライブラリで定義されているstrlen関数に停止するとします:
size_t strlen(const char *s);
次のように、ネイティブ・リンカーを使用して、strlenを公開するダウンコール・メソッド・ハンドルを取得します:
Linker linker = Linker.nativeLinker();
MethodHandle strlen = linker.downcallHandle(
    linker.defaultLookup().find("strlen").orElseThrow(),
    FunctionDescriptor.of(JAVA_LONG, ADDRESS)
);
ネイティブ・リンカーが「デフォルト・ルックアップ」を介して、JavaランタイムとともにロードされるCライブラリによって定義されたネイティブ関数にアクセスする方法も確認します。 前述のデフォルトのルックアップは、strlenネイティブ関数のアドレスを検索するために使用されます。 そのアドレスは、FunctionDescriptor (その下の詳細)として表される関数のシグネチャの「プラットフォーム依存の説明」とともに、ネイティブ・リンカーのdowncallHandle(MemorySegment, FunctionDescriptor, Option...)RESTRICTEDメソッドに渡されます。 取得されたdowncallメソッド・ハンドルは、次のように起動されます:
 try (Arena arena = Arena.ofConfined()) {
     MemorySegment str = arena.allocateFrom("Hello");
     long len = (long) strlen.invokeExact(str);  // 5
 }

シグネチャの説明

ネイティブ・リンカーと対話する場合、クライアントは、リンク先のC関数のシグネチャについてプラットフォームに依存する説明を提供する必要があります。 この説明(function descriptor)は、C関数のパラメータ・タイプおよび戻り型(もしあれば)に関連付けられたレイアウトを定義します。

boolintなどのスカラーC型は、適切なキャリアの「値レイアウト」としてモデル化されます。 スカラー型とそれに対応する正規レイアウトとの間の「マッピング」は、ネイティブ・リンカー(下記参照)によって実装されるABIに依存します。

コンポジット型は、「グループ・レイアウト」としてモデル化されます。 具体的には、C struct型は「構造体レイアウト」にマップされ、C union型はunion layoutにマップされます。 構造体または共用体のレイアウトを定義する場合、クライアントはC内の対応する複合型定義のサイズおよび整列制約に注意する必要があります。 たとえば、2つの構造体フィールド間のパディングは、適切なサイズの「パディング・レイアウト」メンバーを結果の構造体レイアウトに追加することで、明示的にモデル化する必要があります。

最後に、int**int(*)(size_t*, size_t*)などのポインタ型は、アドレス・レイアウトとしてモデル化されます。 ポインタ型の空間境界が静的にわかっている場合は、アドレス・レイアウトを「ターゲット・レイアウト」に関連付けることができます。 たとえば、C int[2]配列を指すことが知られているポインタは、ターゲット・レイアウトが要素数が2で要素タイプがValueLayout.JAVA_INTのシーケンス・レイアウトであるアドレス・レイアウトとしてモデル化できます。

すべてのネイティブ・リンカー実装は、次のタイプのセットに対して正規のレイアウトを提供することが保証されています。

  • bool
  • char
  • short
  • int
  • long
  • long long
  • float
  • double
  • size_t
  • wchar_t
  • void*
前述のように、各タイプに関連付けられた特定の標準レイアウトは、特定のABIでサポートされているデータ・モデルに応じて異なる場合があります。 たとえば、C型longは、Linux/x64のレイアウト定数ValueLayout.JAVA_LONGにマップされますが、Windows/x64のレイアウト定数ValueLayout.JAVA_INTにマップされます。 同様に、C型size_tは、64ビット・プラットフォームではレイアウト定数ValueLayout.JAVA_LONGにマップされますが、32ビット・プラットフォームではレイアウト定数ValueLayout.JAVA_INTにマップされます。

ネイティブ・リンカーは通常、Cの符号なし整数型に正規のレイアウトを提供しません。 かわりに、対応する符号付き整数型に関連付けられた正規のレイアウトを使用してモデル化されます。 たとえば、C型unsigned longは、Linux/x64のレイアウト定数ValueLayout.JAVA_LONGにマップされますが、Windows/x64のレイアウト定数ValueLayout.JAVA_INTにマップされます。

次の表に、"System Vアプリケーション・バイナリ・インタフェース" (ここに示すすべての例では、これらのプラットフォーム依存マッピングを想定しています。)に従ってC型をLinux/x64でモデル化する方法の例を示します。

マッピングCタイプ
Cタイプ レイアウト Javaタイプ
bool ValueLayout.JAVA_BOOLEAN boolean
char
unsigned char
ValueLayout.JAVA_BYTE byte
short
unsigned short
ValueLayout.JAVA_SHORT short
int
unsigned int
ValueLayout.JAVA_INT int
long
unsigned long
ValueLayout.JAVA_LONG long
long long
unsigned long long
ValueLayout.JAVA_LONG long
float ValueLayout.JAVA_FLOAT float
double ValueLayout.JAVA_DOUBLE double
size_t ValueLayout.JAVA_LONG long
char*, int**, struct Point* ValueLayout.ADDRESS MemorySegment
int (*ptr)[10]
 ValueLayout.ADDRESS.withTargetLayout(
     MemoryLayout.sequenceLayout(10,
         ValueLayout.JAVA_INT)
 );
 
MemorySegment
struct Point { int x; long y; };
 MemoryLayout.structLayout(
     ValueLayout.JAVA_INT.withName("x"),
     MemoryLayout.paddingLayout(32),
     ValueLayout.JAVA_LONG.withName("y")
 );
 
MemorySegment
union Choice { float a; int b; }
 MemoryLayout.unionLayout(
     ValueLayout.JAVA_FLOAT.withName("a"),
     ValueLayout.JAVA_INT.withName("b")
 );
 
MemorySegment

すべてのネイティブ・リンカー実装は、明確に定義されたレイアウトのサブセットをサポートします。 より正式には、ネイティブ・リンカーNLで次の場合、レイアウトLがサポートされます:

  • L は値レイアウトVで、V.withoutName()は標準レイアウトです。
  • L は順序レイアウトSで、次のすべての条件が保持されます:
    1. Sの境界整列制約は、その「自然整列」に設定されます
    2. S.elementLayout()は、NLでサポートされているレイアウトです。
  • L はグループ・レイアウトGで、次のすべての条件が保持されます:
    1. Gの整列制約は、その「自然整列」に設定されます
    2. Gのサイズは、その整列制約の倍数です
    3. G.memberLayouts()の各メンバー・レイアウトは、パディング・レイアウトまたはNLでサポートされているレイアウトのいずれかです
    4. G には、非パディング・レイアウト要素の整列、または(2)を満たすために厳密に必要なパディング以外は含まれません。
リンカーの実装では、オプションで、「梱包済」構造体レイアウトなどの追加のレイアウトをサポートできます。 パック構造体は、整列制約が自然整列より厳しくないメンバー・レイアウトLが少なくとも1つ存在する構造体です。 これにより、メンバー・レイアウト間のパディングを回避し、構造体レイアウトの最後にパディングを回避できます。 たとえば、
// No padding between the 2 element layouts:
MemoryLayout noFieldPadding = MemoryLayout.structLayout(
        ValueLayout.JAVA_INT,
        ValueLayout.JAVA_DOUBLE.withByteAlignment(4));

// No padding at the end of the struct:
MemoryLayout noTrailingPadding = MemoryLayout.structLayout(
        ValueLayout.JAVA_DOUBLE.withByteAlignment(4),
        ValueLayout.JAVA_INT);

ネイティブ・リンカーは、引数/戻りレイアウトがそのリンカーでサポートされるレイアウトであり、シーケンス・レイアウトではない関数記述子のみをサポートします。

関数ポインタ

場合によっては、Javaコードを一部のネイティブ関数への関数ポインタとして渡すと便利です。これは、upcall stubRESTRICTEDを使用して実現します。 これを示すために、C標準ライブラリの次の関数について考えてみます:
void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));
qsort関数は、関数ポインタ(comparパラメータ)として渡されるカスタム・コンパレータ関数を使用して、配列の内容をソートするために使用できます。 Javaからqsort関数をコールできるようにするには、まず、次のように、停止コール・メソッド・ハンドルを作成する必要があります:
Linker linker = Linker.nativeLinker();
MethodHandle qsort = linker.downcallHandle(
    linker.defaultLookup().find("qsort").orElseThrow(),
        FunctionDescriptor.ofVoid(ADDRESS, JAVA_LONG, JAVA_LONG, ADDRESS)
);
前述のように、ValueLayout.JAVA_LONGを使用してC型のsize_t型をマップし、ValueLayout.ADDRESSを使用して最初のポインタ・パラメータ(配列ポインタ)と最後のパラメータ(関数ポインタ)の両方をマップします。

上で取得したqsortダウンコール・ハンドルを呼び出すには、関数ポインタを最後のパラメータとして渡す必要があります。 つまり、既存のメソッド・ハンドルから関数ポインタを作成する必要があります。 まず、ポインタ(例:「メモリー・セグメント」)として渡された2つのint要素を比較できるJavaメソッドを記述します。

class Qsort {
    static int qsortCompare(MemorySegment elem1, MemorySegment elem2) {
        return Integer.compare(elem1.get(JAVA_INT, 0), elem2.get(JAVA_INT, 0));
    }
}
次に、上で定義したコンパレータ・メソッドのメソッド・ハンドルを作成します:
FunctionDescriptor comparDesc = FunctionDescriptor.of(JAVA_INT,
                                                      ADDRESS.withTargetLayout(JAVA_INT),
                                                      ADDRESS.withTargetLayout(JAVA_INT));
MethodHandle comparHandle = MethodHandles.lookup()
                                         .findStatic(Qsort.class, "qsortCompare",
                                                     comparDesc.toMethodType());
まず、関数ポインタ型の関数記述子を作成します。 コンパレータ・メソッドに渡されるパラメータは、C int[]配列の要素へのポインタであることがわかっているため、両方のパラメータのアドレス・レイアウトのターゲット・レイアウトとしてValueLayout.JAVA_INTを指定できます。 これによって、比較メソッドが比較する配列要素のコンテンツにアクセスできるようになります。 次に、その関数記述子を適切な「メソッド・タイプ」「なる」し、コンパレータ・メソッド・ハンドルの検索に使用します。 次のように、そのメソッドを指すアップ・コール・スタブを作成し、ファンクション・ポインタとしてqsortダウンコール・ハンドルに渡すことができます。
try (Arena arena = Arena.ofConfined()) {
    MemorySegment comparFunc = linker.upcallStub(comparHandle, comparDesc, arena);
    MemorySegment array = arena.allocateFrom(JAVA_INT, 0, 9, 3, 4, 6, 5, 1, 8, 2, 7);
    qsort.invokeExact(array, 10L, 4L, comparFunc);
    int[] sorted = array.toArray(JAVA_INT); // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
}
このコードは、オフ・ヒープ配列を作成し、その配列にJava配列の内容をコピーし、ネイティブ・リンカーから取得したコンパレータ関数とともに配列をqsortメソッド・ハンドルに渡します。 呼出し後、オフ・ヒープ配列の内容は、Javaで記述されたコンパレータ関数に従ってソートされます。 次に、ソートされた要素を含む新しいJava配列をセグメントから抽出します。

ポインタを返す関数

ネイティブ関数と対話する場合は、これらの関数がメモリーのリージョンを割り当て、その領域へのポインタを返すことが一般的です。 C標準ライブラリの次の関数について考えてみます:
void *malloc(size_t size);
malloc関数は、指定されたサイズのメモリー・リージョンを割り当て、そのメモリー・リージョンへのポインタを返します。このポインタは、後でC標準ライブラリの別の関数を使用して割当て解除されます。
void free(void *ptr);
free関数は、メモリーのリージョンへのポインタを取得し、その領域の割当てを解除します。 この項では、「安全」割当てAPI (次に示すアプローチは、mallocおよびfree以外の割当て関数に一般化できます)の提供を目的として、これらのネイティブ関数との対話方法を示します。

まず、次のように、mallocおよびfreeのダウンコール・メソッド・ハンドルを作成する必要があります:

Linker linker = Linker.nativeLinker();

MethodHandle malloc = linker.downcallHandle(
    linker.defaultLookup().find("malloc").orElseThrow(),
    FunctionDescriptor.of(ADDRESS, JAVA_LONG)
);

MethodHandle free = linker.downcallHandle(
    linker.defaultLookup().find("free").orElseThrow(),
    FunctionDescriptor.ofVoid(ADDRESS)
);
ダウンコール・メソッド・ハンドルを使用してポインタ(malloc)を返すネイティブ・ファンクションが呼び出されると、Javaランタイムは、返されるポインタのサイズまたは存続期間を把握できません。 次のコードについて検討します。
MemorySegment segment = (MemorySegment)malloc.invokeExact(100);
mallocダウンコール・メソッド・ハンドルによって返されるセグメントのサイズは、zeroです。 さらに、返されるセグメントのスコープはグローバル・スコープです。 セグメントに安全にアクセスできるようにするには、セグメントを適切なサイズ(100、この場合は)にサイズ変更する必要があります。 また、Javaコードから直接作成された他のネイティブ・セグメントと同様に、セグメントをバッキングするメモリー・リージョンの存続期間を自動的に管理できるように、既存の「アリーナ」にセグメントをアタッチすることが望ましい場合があります。 これらの操作はどちらも、次のように制限付きメソッドMemorySegment.reinterpret(long, Arena, Consumer)RESTRICTEDを使用して実行されます:
MemorySegment allocateMemory(long byteSize, Arena arena) throws Throwable {
    MemorySegment segment = (MemorySegment) malloc.invokeExact(byteSize); // size = 0, scope = always alive
    return segment.reinterpret(byteSize, arena, s -> {
        try {
            free.invokeExact(s);
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    });  // size = byteSize, scope = arena.scope()
}
前述のallocateMemoryメソッドは、2つのパラメータを受け入れます: サイズとアリーナ。 このメソッドは、mallocダウンコール・メソッド・ハンドルをコールし、新しいサイズ(allocateMemoryメソッドに渡されるサイズ)および新しいスコープ(指定されたアリーナのスコープ)を指定することで、返されたセグメントを安全に再解釈します。 このメソッドは、指定されたアリーナが閉じられるときに実行される「クリーン・アップ処理」も指定します。 驚くべきことに、クリーンアップ・アクションは、セグメントをfreeダウンコール・メソッド・ハンドルに渡して、メモリーの基礎となるリージョンの割当てを解除します。 allocateMemoryメソッドは次のように使用できます:
try (Arena arena = Arena.ofConfined()) {
    MemorySegment segment = allocateMemory(100, arena);
} // 'free' called here
allocateMemoryから取得したセグメントが、限定されたアリーナによって管理される他のセグメントとして機能することに注意してください。 具体的には、取得したセグメントは必要なサイズを持ち、単一のスレッド(狭いアリーナを作成したスレッド)によってのみアクセスでき、その存続期間は周囲のtry-with-resourcesブロックに関連付けられます。

可変個引数関数

可変個引数関数は、変数の数と引数の型を受け入れることができるC関数です。 これらは、仮パラメータ・リストの最後に次の省略記号(...)で宣言されます: void foo(int x, ...);省略記号のかわりに渡される引数は、可変個引数と呼ばれます。 可変個引数関数は、基本的に、...を固定の数と型の「可変個引数パラメータ」のリストに置き換えることで、複数の非変数関数に「特殊化」できるテンプレートです。

可変個引数として渡される値は、Cでデフォルトの引数昇格を受けることに注意してください。 たとえば、次の引数プロモーションが適用されます:

  • _Bool -> unsigned int
  • [signed] char -> [signed] int
  • [signed] short -> [signed] int
  • float -> double
ソース・タイプの署名は、昇格されたタイプの署名状態に対応します。 デフォルトの引数昇格の完全なプロセスについては、Cの仕様を参照してください。 実際、これらのプロモーションでは、...の置換に使用できる型に制限が課されます。これは、可変関数の特殊な形式の可変個パラメータには常に昇格された型があるためです。

ネイティブ・リンカーは、特殊な形式の可変個引数関数のリンクのみをサポートします。 特殊な形式の可変個引数関数を、特殊な形式を記述する関数記述子を使用してリンクできます。 さらに、Linker.Option.firstVariadicArg(int)リンカー・オプションを指定して、パラメータ・リスト内の最初の可変個パラメータを示す必要があります。 対応する引数レイアウト(もしあれば)、および特殊な関数記述子の次のすべての引数レイアウトは、「可変個引数レイアウト」と呼ばれます。

ネイティブ・リンカーは、デフォルトの引数昇格を自動的に実行しません。 ただし、非プロモート型の引数を可変個引数として渡すことはCではサポートされないため、ネイティブ・リンカーは、非プロモートC型に対応する任意の可変個引数値レイアウトと特殊関数記述子をリンクする試みを拒否します。 C intタイプのサイズはプラットフォーム固有であるため、どのレイアウトが拒否されるかもプラットフォーム固有です。 例として: Linux/x64では、C型_Bool, (unsigned) char, (unsigned) shortおよびfloat (とりわけ)に対応するレイアウトはリンカーによって拒否されます。 canonicalLayouts()メソッドを使用すると、特定のC型に対応するレイアウトを検索できます。

既知の可変個引数関数とは、C標準ライブラリで定義されるprintf関数です:

int printf(const char *format, ...);
この関数は、書式文字列、および多数の追加引数(このような引数の数は、書式文字列によって決定されます)を取ります。 次の可変個引数コールについて考えてみます:
printf("%d plus %d equals %d", 2, 2, 4);
ダウンコール・メソッド・ハンドルを使用して同等のコールを実行するには、コールするC関数の特殊なシグネチャを記述する関数記述子を作成する必要があります。 この記述子には、指定する各可変個引数に追加のレイアウトを含める必要があります。 この場合、書式文字列が3つの整数パラメータを受け入れるため、C関数の特殊なシグネチャは(char*, int, int, int)です。 次に、「リンカー・オプション」を使用して、提供された関数記述子(0から開始)内の最初の可変個レイアウトの位置を指定する必要があります。 この場合、最初のパラメータが書式文字列(非可変個引数)であるため、最初の可変個引数索引を次のように1に設定する必要があります:
Linker linker = Linker.nativeLinker();
MethodHandle printf = linker.downcallHandle(
    linker.defaultLookup().find("printf").orElseThrow(),
        FunctionDescriptor.of(JAVA_INT, ADDRESS, JAVA_INT, JAVA_INT, JAVA_INT),
        Linker.Option.firstVariadicArg(1) // first int is variadic
);
その後、通常どおり専用のダウンコール・ハンドルを呼び出すことができます:
 try (Arena arena = Arena.ofConfined()) {
     //prints "2 plus 2 equals 4"
     int res = (int)printf.invokeExact(arena.allocateFrom("%d plus %d equals %d"), 2, 2, 4);
 }

安全に関する考慮事項

ダウンコール・メソッド・ハンドルの作成は本質的に安全ではありません。 一般的に、外部ライブラリ内のシンボルには十分なシグネチャ情報 (例、外部ファンクション・パラメータのアリティおよびタイプ)が含まれていません。 その結果、リンカーのランタイムはリンケージ・リクエストを検証できません。 クライアントが無効なリンケージ・リクエスト(例:引数のレイアウトが多すぎる関数記述子を指定)を介して取得されたダウン・コール・メソッド・ハンドルと対話する場合、このような対話の結果は不特定であり、JVMがクラッシュする可能性があります。

アップコール・スタブが外部関数に渡されると、アップコール・スタブに関連付けられた関数ポインタをアップコール・スタブの型と互換性のない型にキャストし、結果として得られる関数ポインタを介して関数を呼び出そうとすると、JVMクラッシュが発生する可能性があります。 さらに、アップ・コール・スタブに関連付けられたメソッド・ハンドルが「メモリー・セグメント」を返す場合、クライアントは、アップ・コールの完了後にこのアドレスが無効になることがないようにする必要があります。 これは、通常、アップ・コールはダウン・コール・メソッド・ハンドルの起動のコンテキストで実行されるため、不特定の動作を引き起こし、JVMがクラッシュする可能性があります。

実装要件:
このインタフェースの実装は不変、スレッド・セーフ、およびvalue-basedです。
導入されたバージョン:
22