注
この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。
機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。
機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。
概要
この提案では、現在 C# で効率的にアクセスできない、またはまったくアクセスできない IL オペコードを公開する言語コンストラクト ( ldftn と calli) を提供します。 これらの IL オペコードはハイ パフォーマンス コードで重要な場合があり、開発者はそれらに効率的にアクセスする方法が必要です。
モチベーション
この機能の動機と背景については、次の問題で説明します (この機能の潜在的な実装と同様)。
これは、 コンパイラ組み込み関数の代替設計提案です
詳細な設計
関数ポインター
この言語では、 delegate* 構文を使用して関数ポインターを宣言できます。 完全な構文については次のセクションで詳しく説明しますが、 Func および Action 型宣言で使用される構文に似ています。
unsafe class Example
{
void M(Action<int> a, delegate*<int, void> f)
{
a(42);
f(42);
}
}
これらの型は、ECMA-335 で説明されているように、関数ポインター型を使用して表されます。 つまり、delegate*の呼び出しでは、calliの呼び出しが delegate メソッドでcallvirtを使用するInvokeが使用されます。
構文的には、呼び出しは両方のコンストラクトで同じです。
メソッド ポインターの ECMA-335 定義には、型シグネチャの一部として呼び出し規則が含まれています (セクション 7.1)。
既定の呼び出し規則は managedされます。 アンマネージド呼び出し規則は、ランタイム プラットフォームの既定値を使用するunmanaged構文をdelegate*キーワードで推論することで指定できます。 その後、unmanaged名前空間のCallConvで始まる任意の型を指定し、System.Runtime.CompilerServices プレフィックスをオフにすることで、CallConv キーワードに対する特定のアンマネージ規則を角かっこで囲んで指定できます。 これらの型はプログラムのコア ライブラリから取得する必要があり、有効な組み合わせのセットはプラットフォームに依存します。
//This method has a managed calling convention. This is the same as leaving the managed keyword off.
delegate* managed<int, int>;
// This method will be invoked using whatever the default unmanaged calling convention on the runtime
// platform is. This is platform and architecture dependent and is determined by the CLR at runtime.
delegate* unmanaged<int, int>;
// This method will be invoked using the cdecl calling convention
// Cdecl maps to System.Runtime.CompilerServices.CallConvCdecl
delegate* unmanaged[Cdecl] <int, int>;
// This method will be invoked using the stdcall calling convention, and suppresses GC transition
// Stdcall maps to System.Runtime.CompilerServices.CallConvStdcall
// SuppressGCTransition maps to System.Runtime.CompilerServices.CallConvSuppressGCTransition
delegate* unmanaged[Stdcall, SuppressGCTransition] <int, int>;
delegate*型間の変換は、呼び出し規則を含むシグネチャに基づいて行われます。
unsafe class Example {
void Conversions() {
delegate*<int, int, int> p1 = ...;
delegate* managed<int, int, int> p2 = ...;
delegate* unmanaged<int, int, int> p3 = ...;
p1 = p2; // okay p1 and p2 have compatible signatures
Console.WriteLine(p2 == p1); // True
p2 = p3; // error: calling conventions are incompatible
}
}
delegate*型は、標準ポインター型のすべての機能と制限があることを意味するポインター型です。
-
unsafeコンテキストでのみ有効です。 -
delegate*パラメーターまたは戻り値の型を含むメソッドは、unsafeコンテキストからのみ呼び出すことができます。 -
objectに変換できません。 - ジェネリック引数として使用することはできません。
-
delegate*をvoid*に暗黙的に変換できます。 -
void*からdelegate*に明示的に変換できます。
制限:
- カスタム属性は、
delegate*またはその要素には適用できません。 -
delegate*パラメーターを次のようにマークすることはできません。params -
delegate*型には、通常のポインター型のすべての制限があります。 - ポインターの算術演算は、関数ポインター型に対して直接実行することはできません。
関数ポインターの構文
関数ポインターの完全な構文は、次の文法で表されます。
pointer_type
: ...
| funcptr_type
;
funcptr_type
: 'delegate' '*' calling_convention_specifier? '<' funcptr_parameter_list funcptr_return_type '>'
;
calling_convention_specifier
: 'managed'
| 'unmanaged' ('[' unmanaged_calling_convention ']')?
;
unmanaged_calling_convention
: 'Cdecl'
| 'Stdcall'
| 'Thiscall'
| 'Fastcall'
| identifier (',' identifier)*
;
funptr_parameter_list
: (funcptr_parameter ',')*
;
funcptr_parameter
: funcptr_parameter_modifier? type
;
funcptr_return_type
: funcptr_return_modifier? return_type
;
funcptr_parameter_modifier
: 'ref'
| 'out'
| 'in'
;
funcptr_return_modifier
: 'ref'
| 'ref readonly'
;
calling_convention_specifierが指定されていない場合、既定値は managed です。
calling_convention_specifierの正確なメタデータ エンコードと、identifierで有効なunmanaged_calling_conventionについては、「呼び出し規則のMetadata 表現で説明します。
delegate int Func1(string s);
delegate Func1 Func2(Func1 f);
// Function pointer equivalent without calling convention
delegate*<string, int>;
delegate*<delegate*<string, int>, delegate*<string, int>>;
// Function pointer equivalent with calling convention
delegate* managed<string, int>;
delegate*<delegate* managed<string, int>, delegate*<string, int>>;
関数ポインターの変換
安全でないコンテキストでは、使用可能な一連の暗黙的な変換 (暗黙的な変換) が拡張され、次の暗黙的なポインター変換が含まれます。
- 既存の変換 - (§23.5)
-
funcptr_type
F0から別のfuncptr_typeF1まで、次のすべてが当てはまる場合。-
F0およびF1は同じ数のパラメーターを持ち、D0n内の各パラメーターF0は、refの対応するパラメーターoutと同じin、D1n、またはF1修飾子を持ちます。 - 各値パラメーター (
ref、out、またはin修飾子を持たないパラメーター) に対して、id 変換、暗黙的な参照変換、または暗黙的なポインター変換は、F0内のパラメーター型からF1の対応するパラメーター型に存在します。 - 各
ref、out、またはinパラメーターについて、F0のパラメーター型は、F1の対応するパラメーター型と同じです。 - 戻り値の型が値 (
refまたはref readonlyなし) の場合、F1の戻り値の型からF0の戻り値の型への ID、暗黙的な参照、または暗黙的なポインター変換が存在します。 - 戻り値の型が参照 (
refまたはref readonly) の場合、refの戻り値の型およびF1修飾子は、refの戻り値の型およびF0修飾子と同じです。 -
F0の呼び出し規則は、F1の呼び出し規則と同じです。
-
ターゲット メソッドへのアドレス指定を許可する
メソッド グループが address-of 式の引数として許可されるようになりました。 このような式の型は、ターゲット メソッドとマネージド呼び出し規約と同等のシグネチャを持つ delegate* になります。
unsafe class Util {
public static void Log() { }
void Use() {
delegate*<void> ptr1 = &Util.Log;
// Error: type "delegate*<void>" not compatible with "delegate*<int>";
delegate*<int> ptr2 = &Util.Log;
}
}
安全でないコンテキストでは、 M メソッドは、次のすべてが当てはまる場合に F 関数ポインター型と互換性があります。
-
MFは同じ数のパラメーターを持ち、Mの各パラメーターには、refの対応するパラメーターと同じout、in、またはF修飾子があります。 - 各値パラメーター (
ref、out、またはin修飾子を持たないパラメーター) に対して、id 変換、暗黙的な参照変換、または暗黙的なポインター変換は、M内のパラメーター型からFの対応するパラメーター型に存在します。 - 各
ref、out、またはinパラメーターについて、Mのパラメーター型は、Fの対応するパラメーター型と同じです。 - 戻り値の型が値 (
refまたはref readonlyなし) の場合、Fの戻り値の型からMの戻り値の型への ID、暗黙的な参照、または暗黙的なポインター変換が存在します。 - 戻り値の型が参照 (
refまたはref readonly) の場合、refの戻り値の型およびF修飾子は、refの戻り値の型およびM修飾子と同じです。 -
Mの呼び出し規則は、Fの呼び出し規則と同じです。 これには、呼び出し規約ビットと、アンマネージ識別子で指定された呼び出し規則フラグの両方が含まれます。 -
Mは静的メソッドです。
安全でないコンテキストでは、次に示すように、Eのパラメーター型と修飾子を使用して構築された引数リストに通常の形式で適用可能なメソッドが少なくとも 1 つ含まれている F場合E、ターゲットがメソッド グループFメソッド グループから互換性のある関数ポインター型への暗黙的な変換が存在します。
- フォーム
Mのメソッド呼び出しに対応する 1 つのメソッドE(A)が選択され、次の変更が加えられます。- 引数リスト
Aは式のリストであり、それぞれ変数として分類され、対応するrefのfuncptr_parameter_listの型と修飾子 (out、in、またはF) を使用します。 - 候補メソッドは、通常の形式で適用できるメソッドのみで、展開された形式では適用できません。
- 候補メソッドは、静的なメソッドのみです。
- 引数リスト
- オーバーロード解決のアルゴリズムでエラーが発生した場合は、コンパイル時エラーが発生します。 それ以外の場合、アルゴリズムは、
Mと同じ数のパラメーターを持つF1 つの最適な方法を生成し、変換が存在すると見なされます。 - 選択したメソッド
Mは、(上で定義したように) 関数ポインター型Fと互換性がある必要があります。 それ以外の場合は、コンパイル時エラーが発生します。 - 変換の結果は、
F型の関数ポインターです。
つまり、開発者はオーバーロード解決規則に依存して、address-of 演算子と組み合わせて動作させることができます。
unsafe class Util {
public static void Log() { }
public static void Log(string p1) { }
public static void Log(int i) { }
void Use() {
delegate*<void> a1 = &Log; // Log()
delegate*<int, void> a2 = &Log; // Log(int i)
// Error: ambiguous conversion from method group Log to "void*"
void* v = &Log;
}
}
address-of 演算子は、 ldftn 命令を使用して実装されます。
この機能の制限事項:
-
staticとしてマークされたメソッドにのみ適用されます。 -
static以外のローカル関数は、&で使用できません。 これらのメソッドの実装の詳細は、言語によって意図的に指定されていません。 これには、静的とインスタンスのどちらであるか、出力されるシグネチャが正確に含まれます。
関数ポインター型の演算子
式の安全でないコードのセクションは、次のように変更されます。
安全でないコンテキストでは、_funcptr_type_sされていないすべての_pointer_type_sで複数のコンストラクトを操作できます。
*演算子は、ポインター間接参照 (§23.6.2) を実行するために使用できます。->演算子は、ポインター (§23.6.3) を介して構造体のメンバーにアクセスするために使用できます。[]演算子を使用してポインターのインデックスを作成できます (§23.6.4)。&演算子を使用して、変数のアドレスを取得できます (§23.6.5)。++演算子と--演算子は、ポインターのインクリメントとデクリメントに使用できます (§23.6.6)。+演算子と-演算子を使用してポインター算術演算を実行できます (§23.6.7)。- ポインターの比較には、
==、!=、<、>、<=、および=>演算子を使用できます (§23.6.8)。stackalloc演算子を使用して、呼び出し履歴からメモリを割り当てることができます (§23.8)。fixedステートメントを使用して、変数のアドレスを取得できるように変数を一時的に修正できます (§23.7)。安全でないコンテキストでは、すべての_funcptr_type_sで操作するためにいくつかのコンストラクトを使用できます。
&演算子を使用して、静的メソッドのアドレスを取得できます (ターゲット メソッドへのアドレス指定)- ポインターの比較には、
==、!=、<、>、<=、および=>演算子を使用できます (§23.6.8)。
さらに、Pointers in expressionsとPointer comparisonを除き、The sizeof operator内のすべてのセクションを禁止する関数ポインター型に変更します。
ベター関数メンバー
§12.6.4.3 Better 関数メンバー は、次の行を含むように変更されます。
delegate*は、次よりも具体的です。void*
つまり、 void* と delegate* でオーバーロードでき、それでもアドレス演算子を感覚的に使用できます。
型推論
安全でないコードでは、型推論アルゴリズムに次の変更が加えられます。
入力の種類
次のものが追加されます。
Eがメソッド グループのアドレスであり、Tが関数ポインター型の場合、Tのすべてのパラメーター型は、E型を持つTの入力型になります。
出力の種類
次のものが追加されます。
Eがメソッド グループのアドレスであり、Tが関数ポインター型の場合、Tの戻り値の型はE型を持つTの出力型になります。
出力型の推論
箇条書き 2 と 3 の間に次の行頭文字が追加されます。
Eがメソッド グループのアドレスであり、Tがパラメーター型T1...Tkおよび戻り値の型Tbを持つ関数ポインター型であり、戻り値の型Eを持つT1..Tkのオーバーロード解決により、戻り値の型がUされた単一のメソッドが生成される場合、より低い推論はUからTbに対して行われます。
式からの変換の向上
次のサブ行頭文字は、箇条書き 2 にケースとして追加されます。
Vは関数ポインター型delegate*<V2..Vk, V1>で、Uは関数ポインター型delegate*<U2..Uk, U1>であり、Vの呼び出し規則はUと同じであり、Viの参照はUiと同じです。
下限推論
次のケースが箇条書き 3 に追加されます。
Vは関数ポインター型delegate*<V2..Vk, V1>であり、delegate*<U2..Uk, U1>がUと同じで、delegate*<U2..Uk, U1>の呼び出し規則がVと同じであり、Uの参照がViと同じになるように、関数ポインター型Uiがあります。
UiからViへの推論の最初の行頭文字は、次に変更されます。
Uが関数ポインター型ではなく、Uiが参照型であることが不明な場合、またはUが関数ポインター型であり、Uiが関数ポインター型または参照型であることが不明な場合は、exact 推論が行われます
次に、 Ui からの推論の 3 番目の行頭文字の後に、 Viに追加します。
- それ以外の場合、
Vがdelegate*<V2..Vk, V1>場合、推論はdelegate*<V2..Vk, V1>の i 番目のパラメーターに依存します。
- V1 の場合:
- 戻り値が値による場合は、 lower バインド推論 が行われます。
- 戻り値が参照による場合は、 の推定 が行われます。
- V2 の場合..Vk:
- パラメーターが値による場合は、 upper バインド推論 が行われます。
- パラメーターが参照渡しの場合は、 exact 推論 が行われます。
上限推論
箇条書き 2 に次のケースが追加されます。
Uは関数ポインター型delegate*<U2..Uk, U1>で、Vはdelegate*<V2..Vk, V1>と同じ関数ポインター型であり、Uの呼び出し規則はVと同じであり、Uiの参照はViと同じです。
UiからViへの推論の最初の行頭文字は、次に変更されます。
Uが関数ポインター型ではなく、Uiが参照型であることが不明な場合、またはUが関数ポインター型であり、Uiが関数ポインター型または参照型であることが不明な場合は、exact 推論が行われます
その後、 Ui から Viへの推論の 3 番目の行頭文字の後に追加されます。
- それ以外の場合、
Uがdelegate*<U2..Uk, U1>場合、推論はdelegate*<U2..Uk, U1>の i 番目のパラメーターに依存します。
- U1 の場合:
- 戻り値が値による場合は、 upper バインド推論 が行われます。
- 戻り値が参照による場合は、 の推定 が行われます。
- If U2..英国:
- パラメーターが値渡しの場合は、 lower バインド推論 が行われます。
- パラメーターが参照渡しの場合は、 exact 推論 が行われます。
in、out、およびref readonlyパラメーターと戻り値の型のメタデータ表現
関数ポインターシグネチャにはパラメーター フラグの場所がないため、パラメーターと戻り値の型が modreqs を使用して in、 out、または ref readonly かどうかをエンコードする必要があります。
in
パラメーターまたは戻り値の型の ref 指定子にSystem.Runtime.InteropServices.InAttributeとして適用されるmodreqは、次の意味で再利用されます。
- パラメーター ref 指定子に適用された場合、このパラメーターは
inとして扱われます。 - 戻り値の型 ref 指定子に適用された場合、戻り値の型は
ref readonlyとして扱われます。
out
パラメーター型の ref 指定子にSystem.Runtime.InteropServices.OutAttributeとして適用されるmodreqを使用して、パラメーターが out パラメーターであることを意味します。
エラー
-
OutAttributeを modreq として戻り値の型に適用するとエラーになります。 - パラメーター型に modreq として
InAttributeとOutAttributeの両方を適用するとエラーになります。 - modopt を使用していずれかを指定した場合、それらは無視されます。
呼び出し規則のメタデータ表現
呼び出し規則は、シグネチャの CallKind フラグとシグネチャの開始時に 0 個以上の modoptを組み合わせて、メタデータ内のメソッド シグネチャでエンコードされます。 ECMA-335 は現在、 CallKind フラグで次の要素を宣言しています。
CallKind
: default
| unmanaged cdecl
| unmanaged fastcall
| unmanaged thiscall
| unmanaged stdcall
| varargs
;
これらのうち、C# の関数ポインターは、 varargs以外のすべてをサポートします。
さらに、ランタイム (および最終的には 335) が更新され、新しいプラットフォーム上の新しい CallKind が含まれます。 現在、正式な名前はありませんが、このドキュメントでは、新しい拡張可能な呼び出し規約形式を表すプレースホルダーとして unmanaged ext を使用します。
modoptがない場合、unmanaged extはプラットフォームの既定の呼び出し規則であり、角かっこなしでunmanaged。
calling_convention_specifierをCallKind
省略されるか、calling_convention_specifierとして指定されたmanagedは、defaultCallKindにマップされます。 これは、CallKindに属性付けされていないメソッドの既定のUnmanagedCallersOnlyです。
C# は、ECMA 335 から特定の既存のアンマネージド CallKindにマップされる 4 つの特別な識別子を認識します。 このマッピングを行うには、これらの識別子をそれ以外の識別子なしで独自に指定する必要があり、この要件は unmanaged_calling_conventionの仕様にエンコードされます。 これらの識別子は、 Cdecl、 Thiscall、 Stdcall、および Fastcallであり、それぞれ unmanaged cdecl、 unmanaged thiscall、 unmanaged stdcall、および unmanaged fastcallに対応します。 複数の identifer が指定されている場合、または 1 つの identifier が特別に認識された識別子ではない場合は、次の規則を使用して識別子に対して特別な名前検索を実行します。
-
identifierの先頭に文字列を付加します。CallConv -
System.Runtime.CompilerServices名前空間で定義されている型のみを見てみましょう。 - ここでは、アプリケーションのコア ライブラリで定義されている型のみを調べます。これは、
System.Objectを定義し、依存関係を持たないライブラリです。 - パブリック型のみを見てみましょう。
identifierで指定されたすべてのunmanaged_calling_conventionで検索が成功した場合は、CallKindをunmanaged extとしてエンコードし、関数ポインターシグネチャの先頭にある一連のmodoptで解決された各型をエンコードします。 注意として、これらのルールは、ユーザーがこれらの identifierに CallConvのプレフィックスを付けることができないことを意味します。これにより、 CallConvCallConvVectorCallが検索されます。
メタデータを解釈するときは、まず CallKindを見てみましょう。
unmanaged ext以外の場合は、呼び出し規則を決定するために戻り値の型のすべてのmodoptを無視し、CallKindのみを使用します。
CallKindがunmanaged ext場合は、関数ポインター型の先頭にある modopts を見て、次の要件を満たすすべての型の和集合を取得します。
- これはコア ライブラリで定義されています。これは、他のライブラリを参照せず、
System.Objectを定義するライブラリです。 - 型は、
System.Runtime.CompilerServices名前空間で定義されます。 - 型はプレフィックス
CallConvで始まります。 - 型は public です。
これらは、ソースで関数ポインター型を定義するときに、identifier内のunmanaged_calling_conventionに対して検索を実行するときに見つかる必要がある型を表します。
ターゲット ランタイムが機能をサポートしていない場合、CallKindのunmanaged extで関数ポインターを使用しようとするとエラーになります。 これは、 System.Runtime.CompilerServices.RuntimeFeature.UnmanagedCallKind 定数の存在を探すことによって決定されます。 この定数が存在する場合、ランタイムはこの機能をサポートすると見なされます。
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute
System.Runtime.InteropServices.UnmanagedCallersOnlyAttribute は、メソッドを特定の呼び出し規則で呼び出す必要があることを示すために CLR によって使用される属性です。 このため、属性を操作するための次のサポートが導入されています。
- C# からこの属性で注釈が付けられたメソッドを直接呼び出すとエラーになります。 ユーザーは、メソッドへの関数ポインターを取得し、そのポインターを呼び出す必要があります。
- 通常の静的メソッドまたは通常の静的ローカル関数以外に属性を適用するとエラーになります。 C# コンパイラは、メタデータからインポートされた非静的メソッドまたは静的な非通常のメソッドを、この属性を使用して言語でサポートされていないメソッドとしてマークします。
- 属性でマークされたメソッドのパラメーターまたは戻り値の型が
unmanaged_typeではない場合は、エラーになります。 - 属性でマークされたメソッドの型パラメーターが
unmanagedに制限されている場合でも、型パラメーターを持つことはエラーです。 - ジェネリック型のメソッドが属性でマークされるというエラーです。
- 属性でマークされたメソッドをデリゲート型に変換するとエラーになります。
- メタデータ内の呼び出し規則
UnmanagedCallersOnly.CallConvsの要件を満たしていないmodoptの型を指定するとエラーになります。
有効な UnmanagedCallersOnly 属性でマークされたメソッドの呼び出し規則を決定する場合、コンパイラは、 CallConvs プロパティで指定された型に対して次のチェックを実行して、呼び出し規則を決定するために使用する有効な CallKind と modoptを決定します。
- 型が指定されていない場合、
CallKindはunmanaged extとして扱われ、関数ポインター型の先頭に呼び出し規則modoptはありません。 - 指定された型が 1 つあり、その型の名前が
CallConvCdecl、CallConvThiscall、CallConvStdcall、またはCallConvFastcallである場合、CallKindはそれぞれunmanaged cdecl、unmanaged thiscall、unmanaged stdcall、またはunmanaged fastcallとして扱われ、関数ポインター型の先頭に呼び出し規則modoptはありません。 - 複数の型が指定されている場合、または単一の型の名前が上記の特別に呼び出された型の 1 つでない場合、
CallKindはunmanaged extとして扱われ、指定された型の和集合は関数ポインター型の先頭でmodoptとして扱われます。
コンパイラは、この有効な CallKind と modopt コレクションを調べ、通常のメタデータ 規則を使用して、関数ポインター型の最終的な呼び出し規則を決定します。
未解決の質問
のランタイム サポートの検出 unmanaged ext
https://github.com/dotnet/runtime/issues/38135 は、このフラグの追加を追跡します。 レビューからのフィードバックに応じて、問題で指定されたプロパティを使用するか、ランタイムがUnmanagedCallersOnlyAttributeをサポートするかどうかを決定するフラグとしてunmanaged extの存在を使用します。
考慮事項
インスタンス メソッドを許可する
提案は、 EXPLICITTHIS CLI 呼び出し規則 (C# コードでは instance という名前) を利用して、インスタンス メソッドをサポートするように拡張できます。 この形式の CLI 関数ポインターは、 this パラメーターを関数ポインター構文の明示的な最初のパラメーターとして設定します。
unsafe class Instance {
void Use() {
delegate* instance<Instance, string> f = &ToString;
f(this);
}
}
これは音ですが、提案にいくつかの複雑さを追加します。 特に、呼び出し規則 instance と managed によって異なる関数ポインターは、両方のケースが同じ C# シグネチャを持つマネージド メソッドを呼び出すために使用される場合でも互換性がないためです。 また、すべてのケースで、 static ローカル関数を使用するという簡単な回避策を取り入れるのに価値があると考えられます。
unsafe class Instance {
void Use() {
static string toString(Instance i) => i.ToString();
delegate*<Instance, string> f = &toString;
f(this);
}
}
宣言時に安全でない必要はありません
unsafeを使用するたびにdelegate*を要求するのではなく、メソッド グループがdelegate*に変換される時点でのみ必要になります。 ここで、重要な安全性の問題が発生します (値が有効な間は、含まれているアセンブリをアンロードできないことを知っている)。 他の場所で unsafe を要求すると、過剰と見なされる可能性があります。
これは、設計が最初に意図された方法です。 しかし、結果として得られる言語ルールは非常に厄介に感じました。 これがポインター値であり、 unsafe キーワードがなくてもピークを続けているという事実を隠すことはできません。 たとえば、 object への変換を許可できない、 classなどのメンバーにすることはできません。C# 設計では、すべてのポインターの使用に unsafe が必要であるため、この設計はこれに従います。
開発者は、現在の通常のポインター型の場合と同じように、値の上にdelegate* ラッパーを表示できます。 考えてみてください。
unsafe struct Action {
delegate*<void> _ptr;
Action(delegate*<void> ptr) => _ptr = ptr;
public void Invoke() => _ptr();
}
デリゲートの使用
新しい構文要素を使用する代わりに、delegate*、型に続くdelegateを持つ既存の*型を使用するだけです。
Func<object, object, bool>* ptr = &object.ReferenceEquals;
呼び出し規則の処理は、delegate値を指定する属性を使用してCallingConvention型に注釈を付けることで行うことができます。 属性がないと、マネージド呼び出し規則が示されます。
IL でこれをエンコードすることは問題です。 基になる値はポインターとして表される必要があるが、次のことも必要です。
- 異なる関数ポインター型を持つオーバーロードを許可する一意の型を持つ。
- アセンブリ境界を越えた OHI の目的に相当します。
最後の点は特に問題です。 つまり、 Func<int>* を使用するすべてのアセンブリは、 Func<int>* がアセンブリで定義されていても、メタデータ内で同等の型をエンコードする必要がありますが、制御しません。
さらに、mscorlib 以外のアセンブリで System.Func<T> 名前で定義されているその他の型は、mscorlib で定義されているバージョンと異なる必要があります。
調査された 1 つのオプションは、 mod_req(Func<int>) void*などのポインターを出力することでした。 これは、 mod_req が TypeSpec にバインドできないため、ジェネリックインスタンス化をターゲットにできないため、機能しません。
名前付き関数ポインター
関数ポインター構文は、特に入れ子になった関数ポインターのような複雑なケースでは、煩雑になる場合があります。
delegateで行われるように、言語で関数ポインターの名前付き宣言を許可できるたびに、開発者にシグネチャを入力させるのではなく。
func* void Action();
unsafe class NamedExample {
void M(Action a) {
a();
}
}
ここでの問題の一部は、基になる CLI プリミティブに名前がないため、これは純粋に C# の発明であり、有効にするには少しメタデータの作業が必要です。 それは実行可能ですが、作業に関して重要です。 基本的に、C# には、これらの名前に対して型 def テーブルのコンパニオンが必要です。
また、名前付き関数ポインターの引数を調べると、他の多くのシナリオにも同様に適用できることがわかりました。 たとえば、すべてのケースで完全なシグネチャを入力する必要性を減らすために、名前付きタプルを宣言するのと同じくらい便利です。
(int x, int y) Point;
class NamedTupleExample {
void M(Point p) {
Console.WriteLine(p.x);
}
}
議論の後、 delegate* 型の名前付き宣言を許可しないことにしました。 お客様の使用状況に関するフィードバックに基づいて、これに対して大きなニーズがある場合は、関数ポインター、タプル、ジェネリックなどに対して機能する名前付けソリューションを調査します。これは、言語での完全な typedef サポートなどの他の提案と同様の形式である可能性があります。
将来の注意事項
静的デリゲート
これは提案を参照しdelegateメンバーのみを参照できるstatic型の宣言を可能にします。 利点は、このような delegate インスタンスが割り当て不要で、パフォーマンスに依存するシナリオで優れていることです。
関数ポインター機能が実装されている場合、 static delegate 提案は閉じられる可能性があります。この機能の提案された利点は、割り当て無料の性質です。 しかし、最近の調査では、アセンブリのアンロードのために達成できないことがわかりました。 アセンブリがその下からアンロードされないようにするには、 static delegate から参照するメソッドへの強力なハンドルが必要です。
すべての static delegate インスタンスを維持するには、提案の目標に対してカウンターを実行する新しいハンドルを割り当てる必要があります。 呼び出しサイトごとに割り当てを 1 つの割り当てに償却できる設計がいくつかありましたが、それは少し複雑で、トレードオフの価値はないように思えました。
つまり、開発者は基本的に次のトレードオフを決定する必要があります。
- アセンブリのアンロードに直面した場合の安全性: これには割り当てが必要であるため、
delegateは既に十分なオプションです。 - アセンブリのアンロードに対する安全性はありません:
delegate*を使用してください。 これをstructにラップして、コードの残りの部分でunsafeコンテキストの外部で使用できるようにすることができます。
C# feature specifications