Truffleネイティブ関数インタフェース
Truffleには、ネイティブ関数インタフェース(NFI)と呼ばれるネイティブ関数をコールする方法が含まれています。これは、Truffle上に内部言語として実装されており、言語インプリメンタは標準のポリグロットevalインタフェースおよびTruffleの相互運用性を介してアクセスできます。NFIは、たとえば、言語のFFIを実装したり、Javaで使用できないネイティブ・ランタイム・ルーチンをコールするために使用することを目的としています。
NFIはlibffiを使用します。標準のJVMではJNIを使用してコールし、GraalVMネイティブ・イメージではシステムJavaを使用します。今後、コンパイルされたコードからネイティブ・コールが直接行われるように、ネイティブ実行可能ファイルのGraalコンパイラによって最適化される可能性があります。
安定性
NFIは、言語インプリメンタ向けに設計された内部言語です。これは、安定しているとはみなされておらず、インタフェースおよび動作は警告なしで変更される可能性があります。これは、エンドユーザーが直接使用することを目的としていません。
基本的な概念
NFIには、使用している言語のポリグロット・インタフェースを介してアクセスします。これは、JavaまたはTruffle言語です。これにより、Java言語実装コードとゲスト言語の両方からNFIを使用して、記述する必要があるJavaの量を減らすことができます。
エントリ・ポイントは、ポリグロットevalインタフェースです。これは、特別なDSLを実行し、より多くのメソッドを公開できるTruffle相互運用性オブジェクトを返します。
次に、Rubyのポリグロット・インタフェースを使用した例をいくつか示しますが、かわりに他の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コマンドは次のいずれかの形式になります:
defaultload "filename"load (flag | flag | ...) "filename"
defaultコマンドは、プロセスにすでにロードされているすべてのシンボルを含む擬似ライブラリを返します(PosixインタフェースのRTLD_DEFAULTと同等です)。
load "filename"コマンドは、ファイルからライブラリをロードします。ライブラリの命名規則およびロード・パスに関するクロス・プラットフォームの問題には、ユーザーが対応します。
load (flag | flag | ...) "filename" コマンドを使用すると、ライブラリをロードするためのフラグを指定できます。デフォルトのバックエンド(バックエンドについては後で説明します)の場合、およびPosixプラットフォームで実行する場合、使用可能なフラグは、従来のPosixセマンティクスを持つRTLD_GLOBAL、RTLD_LOCAL、RTLD_LAZYおよびRTLD_NOWです。RTLD_LAZYもRTLD_NOWも指定されていない場合、デフォルトはRTLD_NOWです。
ライブラリからのシンボルのロード
ライブラリからシンボルをロードするには、以前にロードされたライブラリ・オブジェクトからシンボルをプロパティとして読み取ります。
symbol = library['symbol_name']
シンボルからのネイティブ関数オブジェクトの生成
ネイティブ関数を呼び出すためにコールできる実行可能オブジェクトを取得するには、Signatureオブジェクトを作成し、bindメソッドをコールして、以前にロードされたシンボル・オブジェクトをバインドします。Signatureオブジェクトは、ネイティブ関数の実際の型シグネチャと一致する必要があります。
signature = Polyglot.eval('nfi', '...signature...')
function = signature.bind(symbol)
シグネチャの形式は(arg, arg, ...) : returnで、argおよびreturnは型です。
型は次の単純型のいずれかになります:
VOIDUINT8SINT8UINT16SINT16UINT32SINT32UINT64SINT64FLOATDOUBLEPOINTERSTRINGOBJECTENV
配列型は、別の型を大カッコ内に配置することによって形成されます。たとえば、[UINT8]です。これらはCスタイルの配列です。
関数ポインタ型は、ネストされたシグネチャを記述することによって形成されます。たとえば、qsortのシグネチャは(POINTER, UINT64, UINT64, (POINTER, POINTER) : SINT32) : VOIDになります。
可変個引数を持つシグネチャを持つ関数の場合は、可変個引数が開始する場所を...で指定しますが、関数をコールする実際の型を指定する必要があります。したがって、異なる型または異なる数の引数を使用してコールするために、同じシンボルを複数回バインドする必要がある場合があります。たとえば、%d %fでprintfをコールするには、型シグネチャ(STRING, ...SINT32, DOUBLE) : SINT32を使用します。
型式は、任意の深さにネストできます。
ENVおよびOBJECTの2つの追加の特殊な型については、このドキュメントの後半のネイティブAPIの項で説明します。
型はどのような場合でも記述できます。
Cなどの外部言語からNFI型への型のマッピングは、ユーザーが行います。
ネイティブ関数オブジェクトのコール
ネイティブ関数をコールするには、次を実行します。
return_value = function.call(...arguments...)
ネイティブ・コードから管理対象関数へのコールバック
ネストされたシグネチャを使用すると、関数コールで関数ポインタを引数として取得できます。管理対象のコール元は、ネイティブ関数ポインタに変換されるポリグロット実行可能オブジェクトを渡す必要があります。この関数ポインタをネイティブ側からコールすると、executeメッセージがポリグロット・オブジェクトに送信されます。
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がない場合、または選択したバックエンドを使用できない場合に使用されます。
実行しているコンポーネントの構成によっては、使用可能なバックエンドに次のものが含まれる場合があります:
nativellvm(GraalVM LLVMランタイムを使用してネイティブ・コードを実行します)panama
Panamaバックエンド
Panamaバックエンドは、プロジェクトPanamaによって導入された外部関数とメモリーAPIを使用します。このバックエンドでは、すべての型のサブセットのみがサポートされます。具体的には、STRING、OBJECT、ENV、FP80または配列型はサポートされていません。機能補完は少なくなりますが、一般にバックエンドのパフォーマンスはより高くなります。現在、JDK 21以降で--enable-previewタグを通じて使用可能です。
ネイティブ・イメージでのTruffle NFI
Truffle NFIを含むネイティブ・イメージをビルドするには、--language:nfi引数を使用するか、native-image.propertiesでRequires = language:NFIを指定するだけです。--language:nfi=<backend>を使用して、nativeバックエンドに使用する実装を選択できます。
--language:NFI=<backend>引数は、Requires = language:NFIを介してNFIを依存性として取得する可能性のある他の引数の前に指定する必要があります。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を参照してください。
マーシャリングの入力
この項では、関数シグネチャのすべての型について、引数値および戻り値がどのように変換されるかを詳しく説明します。
次の表は、NFIシグネチャで使用可能な型と、ネイティブ側の対応するC言語の型、およびこれらの引数が管理対象側のどのポリグロット値にマップされるかを示しています:
| NFIの型 | C言語の型 | ポリグロット値 |
|---|---|---|
VOID |
void |
isNull == trueを持つポリグロット・オブジェクト(戻り型としてのみ有効)。 |
SINT8/16/32/64 |
int8/16/32/64_t |
対応する整数型にfitsIn...するポリグロットisNumber。 |
UINT8/16/32/64 |
uint8/16/32/64_t |
対応する整数型にfitsIn...するポリグロットisNumber。 |
FLOAT |
float |
fitsInFloatのポリグロットisNumber。 |
DOUBLE |
double |
fitsInDoubleのポリグロットisNumber。 |
POINTER |
void * |
isPointer == trueまたはisNull == trueを持つポリグロット・オブジェクト。 |
STRING |
char * (ゼロで終了するUTF-8文字列) |
ポリグロットisString。 |
OBJECT |
TruffleObject |
任意のオブジェクト。 |
[type] |
type * (プリミティブの配列) |
Javaホストのプリミティブ配列。 |
(args):ret |
ret (*)(args) (関数ポインタ型) |
isExecutable == trueを持つポリグロット関数。 |
ENV |
TruffleEnv * |
なし(注入された引数) |
次の項では、型変換について詳しく説明します。
関数ポインタを使用した型変換の動作は、引数の方向が逆になるため、若干混乱を招く可能性があります。疑わしい場合は、引数または戻り値が管理対象からネイティブへ、またはネイティブから管理対象へと流れる方向を常に考えるようにしてください。
VOID
この型は戻り型としてのみ許可され、値を返さない関数を示すために使用されます。
ポリグロットAPIでは、すべての実行可能オブジェクトが値を返す必要があるため、isNull == trueを持つポリグロット・オブジェクトは、VOID戻り型を持つネイティブ関数から返されます。
戻り型がVOIDの管理対象コールバック関数の戻り値は無視されます。
プリミティブ数値
プリミティブ数値型は予想どおりに変換されます。引数はポリグロット数値である必要があり、その値は指定した数値型の値範囲に収まる必要があります。
1つ注意する必要があるのは、符号なし整数型の処理です。ポリグロットAPIが符号なしの型に収まる値に対して個別のメッセージを指定していない場合でも、変換では符号なしの値の範囲が使用されます。たとえば、タイプSINT8の戻り値を介してネイティブから管理対象に渡される値0xFFは、ポリグロット番号-1 (fitsInByte)になります。ただし、UINT8と同じ値が戻されると、ポリグロット番号255になり、fitsInByteではありません。
管理対象コードからネイティブ・コードに数値を渡すと、その数値の符号は無視され、その数値のビットのみが関係します。そのため、たとえば、-1をUINT8型の引数に渡すことは許可され、ネイティブ側の結果は255になります。これは、-1と同じビットを持つためです。逆に、255をSINT8型の引数に渡すことも許可され、ネイティブ側の結果は-1になります。
現在のポリグロットAPIでは、符号付き64ビット範囲外の数値を表すことができないため、現在UINT64型は符号付きセマンティクスで処理されています。これはAPIの既知のバグであり、今後のリリースで変更される予定です。
POINTER
この型は汎用ポインタ引数です。ネイティブ側では、引数の正確なポインタ型は関係ありません。
POINTER引数に渡されたポリグロット・オブジェクトは、可能な場合はネイティブ・ポインタに変換されます(必要に応じてisPointer、asPointerおよびtoNativeメッセージを使用します)。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する必要がある場合があります。返されたポインタを解放すると、返されたポリグロット文字列は無効になり、これを読み取ると未定義の動作になります。そういった意味では、返されたポリグロット文字列は、RAWポインタと同様に安全なオブジェクトではありません。NFIのユーザーは、返された文字列を信頼できない管理対象コードに渡す前に、その文字列をコピーすることをお薦めします。
OBJECT
この引数は、CのTruffleObject型に相当します。この型はtrufflenfi.hで定義されます。これは不透明なポインタ型です。TruffleObject型の値は、任意の管理対象オブジェクトへの参照を表します。
ネイティブ・コードは、戻り値を介して、またはコールバック関数ポインタに渡すことによって管理対象コードに戻す以外は、TruffleObject型の値に対して何もできません。
TruffleObject参照の存続期間は、手動で管理する必要があります。TruffleObject参照の存続期間を管理するAPI関数については、trufflenfi.hのドキュメントを参照してください。
引数として渡されるTruffleObjectはコール元が所有しており、コールの間有効であることが保証されます。コールバック関数ポインタから返されたTruffleObject参照はコール元が所有しており、使用後に解放する必要があります。ネイティブ関数からTruffleObjectを返しても、所有権は譲渡されません(ただし、それを行うためのAPI関数がtrufflenfi.hにあります)。
[...] (ネイティブ・プリミティブ配列)
この型は、管理対象コードからネイティブ関数への引数としてのみ許可され、プリミティブ数値型の配列のみがサポートされます。
管理対象側では、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はサポートされていません。