Truffle ネイティブ関数インタフェース

Truffle には、ネイティブ関数を呼び出す方法として、ネイティブ関数インタフェース (NFI) が含まれています。これは、Truffle 上の内部言語として実装されており、言語実装者は標準の polyglot eval インタフェースと Truffle の相互運用性を通じてアクセスできます。NFI は、たとえば、言語の FFI を実装する場合や、Java では使用できないネイティブランタイムルーチンを呼び出す場合に使用することを目的としています。

NFI は `libffi` を使用します。標準的な JVM では JNI を使用して呼び出し、GraalVM ネイティブイメージではシステム Java を使用します。将来は、ネイティブ実行可能ファイルで Graal コンパイラによって最適化され、ネイティブ呼び出しがコンパイル済みコードから直接行われる可能性があります。

安定性 #

NFI は言語実装者向けに設計された内部言語です。安定したものではなく、インタフェースと動作は予告なく変更される可能性があります。エンドユーザーが直接使用することを目的としたものではありません。

基本概念 #

NFI は、使用している言語の polyglot インタフェースを介してアクセスされます。これは Java でも、Truffle 言語でもかまいません。これにより、Java の実装コードとゲスト言語の両方から NFI を使用して、記述する必要がある Java の量を削減できます。

エントリポイントは polyglot の `eval` インタフェースです。これは特別な DSL を実行し、より多くのメソッドを公開できる Truffle の相互運用オブジェクトを返します。

以下は、Ruby の polyglot インタフェースを使用した例ですが、代わりに他の JVM や言語実装を使用することもできます。

基本的な例 #

詳細に入る前に、基本的な動作例を示します。

library = Polyglot.eval('nfi', 'load "libSDL2.dylib"')  # load a library
symbol = library['SDL_GetRevisionNumber']               # load a symbol from the library
signature = Polyglot.eval('nfi', '():UINT32')           # prepare a signature
function = signature.bind(symbol)                       # bind the symbol to the signature to create a function
puts function.call # => 12373                           # call the function

ライブラリの読み込み #

ライブラリを読み込むには、「`nfi`」言語 DSL で記述されたスクリプトを評価します。これは、読み込まれたライブラリを表すオブジェクトを返します。

library = Polyglot.eval('nfi', '...load command...')

load コマンドは、次のいずれかの形式にすることができます。

  • default
  • load "filename"
  • load (flag | flag | ...) "filename"

`default` コマンドは、プロセスに既に読み込まれているすべてのシンボルを含む擬似ライブラリを返し、Posix インタフェースの `RTLD_DEFAULT` と同等です。

`load "filename"` コマンドは、ファイルからライブラリを読み込みます。ライブラリの名前付け規則とロードパスのクロスプラットフォームに関する問題は、ユーザーが対応する必要があります。

`load (flag | flag | ...) "filename"` コマンドを使用すると、ライブラリを読み込むためのフラグを指定できます。デフォルトのバックエンド(バックエンドについては後で説明します)と、Posix プラットフォームで実行する場合、使用可能なフラグは `RTLD_GLOBAL`、`RTLD_LOCAL`、`RTLD_LAZY`、`RTLD_NOW` であり、従来の Posix セマンティクスを持ちます。`RTLD_LAZY` と `RTLD_NOW` のどちらも指定されていない場合は、デフォルトは `RTLD_NOW` です。

ライブラリからのシンボルの読み込み #

ライブラリからシンボルを読み込むには、以前に読み込まれたライブラリオブジェクトからプロパティとしてシンボルを読み取ります。

symbol = library['symbol_name']

シンボルからネイティブ関数オブジェクトの作成 #

ネイティブ関数を呼び出すために呼び出すことができる実行可能オブジェクトを取得するには、以前に読み込まれたシンボルオブジェクトをバインドします。これには、シグネチャオブジェクトを作成し、そのオブジェクトの `bind` メソッドを呼び出します。シグネチャオブジェクトは、ネイティブ関数の実際の型シグネチャと一致する必要があります。

signature = Polyglot.eval('nfi', '...signature...')
function = signature.bind(symbol)

シグネチャの形式は `(arg, arg, ...) : return` で、`arg` と `return` は型です。

型は、次の単純な型のいずれかになります。

  • VOID
  • UINT8
  • SINT8
  • UINT16
  • SINT16
  • UINT32
  • SINT32
  • UINT64
  • SINT64
  • FLOAT
  • DOUBLE
  • POINTER
  • STRING
  • OBJECT
  • ENV

配列型は、別の型を角括弧で囲むことで形成されます。たとえば `[UINT8]` です。これらは C スタイルの配列です。

関数ポインタ型は、ネストされたシグネチャを書くことで形成されます。たとえば、`qsort` のシグネチャは `(POINTER, UINT64, UINT64, (POINTER, POINTER) : SINT32) : VOID` になります。

可変個の引数を持つ関数のシグネチャの場合、可変個の引数が始まる場所に `...` を指定しますが、その場合は関数を呼び出す際に使用する実際の型を指定する必要があります。したがって、異なる型または異なる数の引数で関数を呼び出すために、同じシンボルを複数回バインドする必要がある場合があります。たとえば、`%d %f` を使用して `printf` を呼び出すには、型シグネチャ `(STRING, ...SINT32, DOUBLE) : SINT32` を使用します。

型式は任意の深さでネストできます。

追加の特別な型 `ENV` と `OBJECT` については、このドキュメントの後半にあるネイティブAPIに関するセクションで説明します。

型は大文字小文字を区別しません。

Cなどの外部言語からNFI型への型のマッピングは、ユーザーが担当します。

ネイティブ関数オブジェクトの呼び出し #

ネイティブ関数を呼び出すには、実行します。

return_value = function.call(...arguments...)

ネイティブコードからマネージド関数へのコールバック #

ネストされたシグネチャを使用すると、関数呼び出しは関数ポインタを引数として取得できます。マネージド呼び出し元は、ネイティブ関数ポインタに変換される Polyglot 実行可能オブジェクトを渡す必要があります。ネイティブ側からこの関数ポインタを呼び出すと、`execute` メッセージが Polyglot オブジェクトに送信されます。

void native_function(int32_t (*fn)(int32_t)) {
  printf("%d\n", fn(15));
}
signature = Polyglot.eval('nfi', '((SINT32):SINT32):VOID')
native_function = signature.bind(library['native_function'])
native_function.call(->(x) { x + 1 })

コールバック関数の引数と戻り値は、通常の関数呼び出しと同様に変換されます。変換は逆方向に行われます。つまり、引数はネイティブからマネージドに変換され、戻り値はマネージドからネイティブに変換されます。

コールバック関数ポインタは、それ自体が関数ポインタ引数を持つことができます。これは予想どおりに機能します。関数はネイティブ関数ポインタを引数として受け入れ、Truffle 実行可能オブジェクトに変換されます。そのオブジェクトに `execute` メッセージを送信すると、通常の NFI 関数を呼び出すのと同様に、ネイティブ関数ポインタが呼び出されます。

関数ポインタ型は戻り値型としてもサポートされています。

読み込みとバインディングの組み合わせ #

ライブラリの読み込みとシンボルの読み込みとバインディングを組み合わせることもできます。これは拡張された `load` コマンドで行われ、既にバインドされた関数をメソッドとして持つオブジェクトを返します。

これらの2つの例は同等です。

library = Polyglot.eval('nfi', 'load libSDL2.dylib')
symbol = library['SDL_GetRevisionNumber']
signature = Polyglot.eval('nfi', '():UINT32')
function = signature.bind(symbol)
puts function.call # => 12373
library = Polyglot.eval('nfi', 'load libSDL2.dylib { SDL_GetRevisionNumber():UINT32; }')
puts library.SDL_GetRevisionNumber # => 12373

中括弧 `{}` 内の定義には複数の関数バインディングを含めることができるため、多くの関数を一度にライブラリから読み込むことができます。

バックエンド #

特定の NFI バックエンドを選択するには、`load` コマンドに `with` をプレフィックスとして付けることができます。複数の NFI バックエンドを使用できます。デフォルトは `native` と呼ばれ、`with` プレフィックスがない場合、または選択したバックエンドを使用できない場合に使用されます。

実行中のコンポーネントの構成によっては、使用可能なバックエンドには以下が含まれる場合があります。

  • native
  • `llvm`。これは、GraalVM LLVM ランタイムを使用してネイティブコードを実行します。
  • panama

Panama バックエンド #

Panama バックエンドは、Project Panamaで導入された外部関数とメモリAPIを使用します。このバックエンドは、すべての型のサブセットのみをサポートします。具体的には、`STRING`、`OBJECT`、`ENV`、`FP80`、および配列型はサポートしていません。機能が限定的ですが、通常はパフォーマンスが向上します。現在、`--enable-preview` タグを使用して JDK 21 以降で使用できます。

ネイティブイメージでの Truffle NFI #

Truffle NFI を含むネイティブイメージをビルドするには、`--language:nfi` 引数を使用するか、`native-image.properties` で `Requires = language:nfi` を指定するだけで十分です。`--language:nfi=<backend>` を使用して、`native` バックエンドに使用する実装を選択できます。

`Requires = language:nfi` を介して NFI を依存関係として取り込む可能性のある他の引数の前に、`--language:nfi=<backend>` 引数を指定する必要があります。最初の `language:nfi` のインスタンスが優先され、ネイティブイメージにビルドされるバックエンドが決まります。

`--language:nfi=<backend>` で使用できる引数は次のとおりです。

  • `libffi` (デフォルト)
  • none

`none` ネイティブバックエンドを選択すると、Truffle NFI を使用したネイティブ関数へのアクセスが事実上無効になります。これにより、ネイティブアクセスに依存する NFI のユーザー(例:GraalVM LLVM ランタイム(EE で `--llvm.managed` を使用する場合を除く))が機能しなくなります。

ネイティブAPI #

NFI は、変更されていない既にコンパイルされたネイティブコードで使用できますが、ネイティブコードで使用される Truffle 固有の API とも使用できます。

特別な型 `ENV` は、シグネチャに `TruffleEnv *env` という追加のパラメータを追加します。追加の単純型 `OBJECT` は、不透明な `TruffleObject` 型に変換されます。

`trufflenfi.h` ヘッダーファイルは、これらの型を操作するための宣言を提供しており、NFI を介して呼び出されるネイティブコードで使用できます。この API の詳細については、`trufflenfi.h` を参照してください。

型マーシャリング #

このセクションでは、関数シグネチャ内のすべての型の引数値と戻り値がどのように変換されるかを詳細に説明します。

次の表は、ネイティブ側の対応する C 言語型と、これらの引数がマネージド側で対応する polyglot 値をマッピングする方法を示した、NFI シグネチャ内の可能な型を示しています。

NFI 型 C 言語型 Polyglot 値
VOID void `isNull == true` の Polyglot オブジェクト(戻り値型としてのみ有効)。
SINT8/16/32/64 int8/16/32/64_t 対応する整数型に `fitsIn...` する `isNumber` の Polyglot。
UINT8/16/32/64 uint8/16/32/64_t 対応する整数型に `fitsIn...` する `isNumber` の Polyglot。
FLOAT float `fitsInFloat` する `isNumber` の Polyglot。
DOUBLE double `fitsInDouble` する `isNumber` の Polyglot。
POINTER void * `isPointer == true` または `isNull == true` の Polyglot オブジェクト。
STRING `char *` (ゼロ終端の UTF-8 文字列) Polyglot の `isString`。
OBJECT TruffleObject 任意のオブジェクト。
[type] type *(プリミティブ型の配列) Javaホストのプリミティブ型配列。
(args):ret ret (*)(args)(関数ポインタ型) isExecutable == trueであるポリグロット関数。
ENV TruffleEnv * なし(挿入引数)

以降のセクションでは、型変換の詳細について説明します。

関数ポインタを使用する際の型変換の動作は、引数の向きが逆になるため、やや分かりにくい場合があります。不明な場合は、常に引数または戻り値のフローの方向(マネージドからネイティブへ、またはネイティブからマネージドへ)を把握するようにしてください。

VOID #

この型は戻り値型としてのみ許可され、値を返さない関数を示すために使用されます。

Polyglot APIでは、すべての実行可能オブジェクトが値を返す必要があるため、戻り値の型がVOIDであるネイティブ関数からは、isNull == trueであるPolyglotオブジェクトが返されます。

戻り値の型がVOIDであるマネージドコールバック関数の戻り値は無視されます。

プリミティブ数値 #

プリミティブ数値型は、予想されるように変換されます。引数はPolyglot数値である必要があり、その値は指定された数値型の値の範囲に収まる必要があります。

注意すべき点の1つは、符号なし整数型の処理です。Polyglot APIは符号なし型に適合する値に対して個別のメッセージを指定しませんが、変換は依然として符号なし値の範囲を使用しています。たとえば、SINT8型の戻り値を介してネイティブからマネージドに渡される値0xFFは、Polyglot数値-1fitsInByteに適合する)になります。しかし、同じ値をUINT8として返すと、Polyglot数値255fitsInByteに適合しない)になります。

マネージドコードからネイティブコードに数値を渡す場合、数値の符号は無視され、数値のビットのみが関連します。そのため、たとえば、UINT8型の引数に-1を渡すことが許可されており、ネイティブ側の結果は255になります(これは-1と同じビットを持つためです)。逆に、SINT8型の引数に255を渡すことも許可されており、ネイティブ側の結果は-1になります。

現在のPolyglot APIでは、符号付き64ビット範囲外の数値を表すことができないため、UINT64型は現在符号付きセマンティクスで処理されます。これはAPIの既知のバグであり、将来のリリースで変更される予定です。

POINTER #

この型は汎用ポインタ引数です。ネイティブ側では、引数の正確なポインタ型は問題ではありません。

POINTER引数に渡されるポリグロットオブジェクトは、可能な場合はネイティブポインタに変換されます(必要に応じてisPointerasPointertoNativeメッセージを使用します)。isNull == trueであるオブジェクトは、ネイティブのNULLとして渡されます。

POINTER戻り値は、isPointer == trueであるポリグロットオブジェクトを生成します。ネイティブのNULLポインタには、さらにisNull == trueが設定されます。

STRING #

これは、文字列の特別な変換セマンティクスを持つポインタ型です。

STRING型を使用してマネージドからネイティブに渡されるポリグロット文字列は、ヌル終端されたUTF-8エンコード文字列に変換されます。STRING引数の場合、ポインタは呼び出し元が所有し、呼び出しの期間中のみ有効であることが保証されます。マネージド関数ポインタからネイティブ呼び出し元に返されるSTRING値も、呼び出し元が所有します。使用後はfreeで解放する必要があります。

ポリグロットポインタ値またはnull値もSTRING引数に渡すことができます。セマンティクスはPOINTER引数の場合と同じです。ポインタが有効なUTF-8文字列であることを保証するのは、ユーザーの責任です。

ネイティブ関数からマネージドコードに渡されるSTRING値は、POINTER戻り値のように動作しますが、さらにisString == trueが設定されます。ポインタの所有権はユーザーの責任であり、呼び出されたネイティブ関数のセマンティクスによっては、戻り値をfreeする必要がある場合があります。返されたポインタを解放した後、返されたポリグロット文字列は無効になり、読み取ると未定義の動作になります。この意味で、返されたポリグロット文字列は、生のポインタと同様に安全なオブジェクトではありません。信頼できないマネージドコードに渡す前に、NFIのユーザーが返された文字列をコピーすることをお勧めします。

OBJECT #

この引数は、C型TruffleObjectに対応します。この型はtrufflenfi.hで定義されており、不透明なポインタ型です。TruffleObject型の値は、任意のマネージドオブジェクトへの参照を表します。

ネイティブコードは、TruffleObject型の値に対して、戻り値を介して、またはコールバック関数ポインタに渡すこと以外に何もできません。

TruffleObject参照のライフタイムは、手動で管理する必要があります。TruffleObject参照のライフタイムを管理するためのAPI関数については、trufflenfi.hのドキュメントを参照してください。

引数として渡されるTruffleObjectは、呼び出し元が所有し、呼び出しの期間中有効であることが保証されます。コールバック関数ポインタから返されるTruffleObject参照は、呼び出し元が所有し、使用後は解放する必要があります。ネイティブ関数からTruffleObjectを返すことは、所有権の移転を意味しません(ただし、trufflenfi.hにはそれを行うためのAPI関数があります)。

[...](ネイティブプリミティブ配列) #

この型は、マネージドコードからネイティブ関数への引数としてのみ許可され、プリミティブ数値型の配列のみがサポートされます。

マネージド側では、Javaプリミティブ型配列を含むJavaホストオブジェクトのみがサポートされます。ネイティブ側では、この型は配列の内容へのポインタです。配列の長さを個別の引数として渡すのは、ユーザーの責任です。

ポインタは、ネイティブ呼び出しの期間中のみ有効です。

呼び出しから戻った後、内容の変更はJava配列に反映されます。ネイティブ呼び出し中にJava配列への同時アクセスによる影響は、未定義です。

(...):...(関数ポインタ) #

ネイティブ側では、ネストされたシグネチャ型は、指定されたシグネチャを持つ関数ポインタ(マネージドコードを呼び戻す)に対応します。

関数ポインタ型を使用してマネージドからネイティブに渡されるポリグロット実行可能オブジェクトは、ネイティブコードで呼び出すことができる関数ポインタに変換されます。関数ポインタ引数の場合、関数ポインタは呼び出し元が所有し、呼び出しの期間中のみ有効であることが保証されます。関数ポインタ戻り値は呼び出し元が所有し、手動で解放する必要があります。関数ポインタ値のライフタイムを管理するためのAPI関数については、polyglot.hを参照してください。

ポリグロットポインタ値またはnull値も関数ポインタ引数に渡すことができます。セマンティクスはPOINTER引数の場合と同じです。ポインタが有効な関数ポインタであることを保証するのは、ユーザーの責任です。

関数ポインタ戻り値の型は通常のPOINTER戻り値の型と同じですが、さらに指定されたシグネチャ型に既にバインドされています。これらはexecuteメッセージをサポートし、通常のNFI関数と同じように動作します。

ENV #

この型は、TruffleEnv *型の特別な引数です。戻り値型としては無効で、引数型としてのみ有効です。ネイティブ側では挿入引数であり、マネージド側には対応する引数はありません。

ネイティブ関数の引数型として使用される場合、ネイティブ関数はこの位置に環境ポインタを取得します。この環境ポインタを使用してAPI関数を呼び出すことができます(trufflenfi.hを参照)。たとえば、シグネチャが(SINT32, ENV, SINT32):VOIDの場合、この関数オブジェクトは2つの整数引数で呼び出されることが想定されており、対応するネイティブ関数は3つの引数で呼び出されます。最初の実際の引数、次に挿入されたENV引数、そして2番目の実際の引数です。

ENV型が関数ポインタパラメータの引数型として使用される場合、その関数ポインタは有効なNFI環境を引数として呼び出す必要があります。呼び出し元に既に環境がある場合、コールバック関数ポインタにそれをスレッド化することは、ENV引数なしで呼び出すよりも効率的です。

呼び出し規約 #

ネイティブ関数は、システムの標準ABIを使用する必要があります。代替ABIは現在サポートされていません。

お問い合わせ