火曜日, 4月 29, 2025
ホームニューステックニュースP/Invoke でのコールバックあれこれ

P/Invoke でのコールバックあれこれ


P/Invoke でネイティブライブラリーから .NET (C#) 側のメソッドを呼び出すコールバックを実装する際の手法とテクニック、デリゲートの取り扱いに疑問があったので改めて調べてまとめてみました。

ここで説明するコードは以下のような C 言語のライブラリー関数を呼び出すことを想定しています。これは Win32 API の EnumWindows のようなコールバックを受け取る API などと同じような形です。


typedef void (*CallbackFunction)();


extern "C" __declspec(dllexport) void InvokeCallback(CallbackFunction callback);


void InvokeCallback(CallbackFunction callback) {
    if (callback) {
        callback();
    }
}

まずは一番スタンダードな手法であるデリゲートをそのままコールバック引数として取り扱う手法です。これは .NET 側でネイティブ側のコールバック型と同じシグネチャーのデリゲートを定義して P/Invoke の関数定義のシグネチャーに指定します。

public static class NativeMethods
{
    
    public delegate void CallbackDelegate();

    [DllImport("MyLibrary1")]
    public static extern void InvokeCallback(CallbackDelegate cb);
}

呼び出しも簡単で特に深いことを考えず、そのままメソッドを渡す形で呼び出せます。


NativeMethods.InvokeCallback(MyClass.CallbackStatic);


var obj = new MyClass();
NativeMethods.InvokeCallback(obj.Callback);

public class MyClass
{
    public void Callback()
    {
        Console.WriteLine("Callback: Callback invoked!");
    }

    public static void CallbackStatic()
    {
        Console.WriteLine("CallbackStatic: Callback invoked!");
    }
}

二つ目の手法は Marshal.GetFunctionPointerForDelegate を使用する手法です。これはデリゲートから関数ポインター (IntPtr) を取得して、ネイティブ側に渡す、という形になります。

この場合、P/Invoke の関数定義ではデリゲートを直接受け取る代わりに IntPtr を受け取るようにします。

public static class NativeMethods
{
    
    public delegate void CallbackDelegate();

    [DllImport("MyLibrary1")]
    public static extern void InvokeCallback(IntPtr cb);
}

呼び出す側はデリゲートのインスタンスを作り、それから関数ポインターを取得し、ネイティブに渡して呼び出します。

{
    
    
    var d = new NativeMethods.CallbackDelegate(MyClass.CallbackStatic);
    
    var ptr = Marshal.GetFunctionPointerForDelegate(d);
    
    NativeMethods.InvokeCallback(ptr);
    
    GC.KeepAlive(d);
}
{
    
    var obj = new MyClass();
    
    var d = new NativeMethods.CallbackDelegate(obj.Callback);
    
    var ptr = Marshal.GetFunctionPointerForDelegate(d);
    
    NativeMethods.InvokeCallback(ptr);
    
    GC.KeepAlive(d);
}

このパターンでは「作ったデリゲート」と「関数ポインター」は特に関連付けられていないので、デリゲートが GC によって回収されないように GC.KeepAlive によって保持しておく必要があります。例えば上記の例だと Marshal.GetFunctionPointerForDelegate の後は変数 d を誰も使わないので何もなければ回収できてしまい、無効な関数ポインターとなる可能性があるということです。

Marshal.GetFunctionPointerForDelegate のドキュメントにも以下のように書かれています。

You must manually keep the delegate from being collected by the garbage collector from managed code. The garbage collector does not track references to unmanaged code.

手法1ではランタイムがデリゲートをいい感じに関数ポインターに変換してくれることでこの辺りは隠されていたとも言えます。

LibraryImport

ところで P/Invoke を使用するには DllImport を使った定義に加えて、.NET 7 以降では Native AOT フレンドリーな形で P/Invoke を使用するために LibraryImport という仕組みが導入されています。

この GetFunctionPointerForDelegate による手法は LibraryImport を使用して、手法1と同様にデリゲートを直接渡す形にした場合に Source Generator で生成されるコードとほぼ同じです。

[LibraryImport("MyLibrary1")] 
public static partial void InvokeCallback(CallbackDelegate cb); 

LibraryImport を使用するように構成すると、下記のコードが Source Generator によって生成されます。


public static unsafe partial class NativeMethods
{
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Interop.LibraryImportGenerator", "8.0.12.16413")]
    [global::System.Runtime.CompilerServices.SkipLocalsInitAttribute]
    public static partial void InvokeCallback(global::NativeMethods.CallbackDelegate cb)
    {
        System.IntPtr __cb_native;
        
        __cb_native = cb != null ? global::System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate(cb) : default;
        {
            __PInvoke(__cb_native);
        }

        
        global::System.GC.KeepAlive(cb);
        
        [global::System.Runtime.InteropServices.DllImportAttribute("MyLibrary1", EntryPoint = "InvokeCallback", ExactSpelling = true)]
        static extern unsafe void __PInvoke(System.IntPtr __cb_native);
    }
}

最後は C# 9 で導入された関数ポインターを使用する手法です。これは静的メソッドから直接関数ポインターを取得して渡すことができるのでデリゲートを作成する必要がありません。

関数ポインターを使うには P/Invoke の関数定義でコールバックとなる引数に delegate* 構文で関数の型を指定します。

public static partial class NativeMethods
{
    [DllImport("MyLibrary1", EntryPoint = "InvokeCallback")]
    public static extern unsafe void InvokeCallback(delegate* unmanagedvoid> cb);
}

そしてマネージ側では呼び出されるメソッドに UnmanagedCallersOnly 属性を付けます。このメソッドは静的なメソッドである必要がある点に注意してください。

public class MyClass
{
    [UnmanagedCallersOnly]
    public static void CallbackStaticMethodForFuncPtr()
    {
        Console.WriteLine("CallbackStaticMethodForFuncPtr: Callback invoked!");
    }
}

そしてネイティブ関数の呼び出し時に & を使用して関数ポインターを取得し、渡します。


NativeMethods.InvokeCallback(&MyClass.CallbackStaticMethodForFuncPtr);

これは静的なメソッドかつデリゲートを生成しないので GC.KeepAlive は必要ありません。

ここまでのコールバックパターンはある特定のマネージコードメソッドからネイティブコードを呼ぶとマネージコードを呼び返されるという流れでした。しかし API などによってはコールバックをネイティブに登録して、ネイティブ側が必要に応じて任意のタイミングで呼び返してくるといったパターンもあります。この場合は呼び出し元のスコープが一度途切れることになります。

Win32 API であればウィンドウプロシージャはその一例ですが、他にもネイティブライブラリー側から通知を受けるというパターンは多く存在します。

以下は RegisterCallback という関数でコールバックを登録して InvokeRegisteredCallback という関数を呼ぶと登録されたコールバックがネイティブ側から呼ばれるという例です。


typedef void (*CallbackFunction)();


extern "C" __declspec(dllexport) void RegisterCallback(CallbackFunction callback);

extern "C" __declspec(dllexport) void InvokeRegisteredCallback();


static CallbackFunction registeredCallback = nullptr;
void RegisterCallback(CallbackFunction callback) {
    registeredCallback = callback;
}

void InvokeRegisteredCallback() {
    if (registeredCallback) {
        registeredCallback();
    }
}

実際は InvokeRegisteredCallback のような関数を呼ぶのではなく、何らかのイベントが発生した際にネイティブ側から呼び出されるパターンとなるはずですが今回は例ということでこのような形にしています。

このパターンでの注意点はコールバックを登録する手法として手法1と2を使用した場合、デリゲートが GC によって回収されてしまう可能性があるということです。(手法3は元々静的な関数ポインターなので回収するものがない)

var obj = new MyClass();


Register(obj);



NativeMethods.InvokeRegisteredCallback();


GC.Collect();



NativeMethods.InvokeRegisteredCallback(); 

static void Register(MyClass obj)
{
    NativeMethods.RegisterCallback(obj.Callback); 
}

このコードは実行すると GC によってデリゲートが回収されているため二度目のコールバック呼び出しで例外をスローします。

この問題に対処する方法としてはデリゲートをフィールドに保持しておく、GCHandle.Alloc を使うといったものがあります。GCHandle.Alloc を使用した例は下記のとおりです。

var obj = new MyClass();
var handle = Register(obj);
NativeMethods.InvokeRegisteredCallback(); 
GC.Collect();
NativeMethods.InvokeRegisteredCallback(); 


handle.Free();

static GCHandle Register(MyClass obj)
{
    
    NativeMethods.CallbackDelegate cb = obj.Callback;
    
    var handle = GCHandle.Alloc(cb);
    
    NativeMethods.RegisterCallback(cb);

    return handle;
}

また、これは静的メソッドをコールバックとする場合 C# 11 未満では同様の問題が発生します。


Register();



NativeMethods.InvokeRegisteredCallback();


GC.Collect();



NativeMethods.InvokeRegisteredCallback(); 

static void Register()
{
    
    NativeMethods.RegisterCallback(MyClass.CallbackStatic);
}

これは static readonly フィールドにデリゲートを保持しておくことで GC に回収されないようにできます。

public class MyClass
{
    ...
    
    public static readonly NativeMethods.CallbackDelegate CallbackStaticDelegate = CallbackStatic;
    ...
}
Register();
NativeMethods.InvokeRegisteredCallback(); 
GC.Collect();
NativeMethods.InvokeRegisteredCallback(); 

static void Register()
{
    NativeMethods.RegisterCallback(MyClass.CallbackStaticDelegate);
}

C# 11 以降であれば静的メソッドのデリゲートはコンパイラーが static readonly フィールドにキャッシュするようになっています。

静的メソッドをコールバックとして扱う場合に何らかのステートを持ったマネージオブジェクトを操作したいケースがあります。特に UnmanagedCallersOnlyMonoPInvokeCallback を使用する場合はそのコールバックメソッドは静的メソッドである必要があるのでこの制約にぶつかることがあります。

例えば呼び出し側はユーザーごとに異なるデータを持っていて、コールバックの中でユーザーの情報にアクセスしたい、といったケースです。この場合コールバックが静的メソッドである都合、this のようなインスタンスアクセスはできません。

この場合、あくまでネイティブライブラリー側が考慮している前提、つまりネイティブライブラリーを自作しているか、下記の解決策のようなものを想定している場合に限りますが解決策がいくつかあります。

解決策1: 識別子と ConcurrentDictionary を使用する

簡単な解決方法の一つは ConcurrentDictionary に識別子をキーにしてデータを保持しておき、コールバックの引数としてその識別子を渡す方法です。


typedef void (*CallbackFunctionWithState)(int id); 


extern "C" __declspec(dllexport) void RegisterCallback(CallbackFunctionWithState callback);

extern "C" __declspec(dllexport) void InvokeRegisteredCallback(int id);


static CallbackFunctionWithState registeredCallbackWithState = nullptr;
void RegisterCallbackWithState(CallbackFunctionWithState callback) {
    registeredCallbackWithState = callback;
}

void InvokeRegisteredCallbackWithState(int id) {
    if (registeredCallback) {
        registeredCallbackWithState(id);
    }
}
public static partial class NativeMethods
{
    public delegate void CallbackDelegateWithState(int id);

    [DllImport("MyLibrary1")]
    public static extern void RegisterCallbackWithState(CallbackDelegateWithState cb);

    [DllImport("MyLibrary1")]
    public static extern void InvokeRegisteredCallbackWithState();
}

public class MyClass
{
    public static ConcurrentDictionaryint, MyState> States { get; } = new();

    [UnmanagedCallersOnly]
    [MonoPInvokeCallback]
    public static void CallbackWithState(int id)
    {
        
        if (States.TryGetValue(id, out var state))
        {
            
        }
    }
}

public class MyState
{
}
NativeMethods.RegisterCallbackWithState(MyClass.CallbackWithState);


MyClass.States[1] = new MyState();



NativeMethods.InvokeRegisteredCallbackWithState(1);

解決策2: GCHandle を使用する

ConcurrentDictionary などを使用せずとも GCHandle を使用することでマネージオブジェクトを保持してポインターの形で取り扱えるようにできます。識別子の代わりにマネージオブジェクトのハンドルを受け渡し、ハンドルを元にマネージオブジェクトを取得します。

ネイティブ側ではポインターを受け取るようにし、P/Invoke の定義もポインター (IntPtr) を渡せるようにします。

typedef void (*CallbackFunctionWithState)(void* state);

extern "C" __declspec(dllexport) void RegisterCallbackWithState(CallbackFunctionWithState callback);
extern "C" __declspec(dllexport) void InvokeRegisteredCallbackWithState(void* state);
public static partial class NativeMethods
{
    public delegate void CallbackDelegateWithState(IntPtr id);

    [DllImport("MyLibrary1")]
    public static extern void RegisterCallbackWithState(CallbackDelegateWithState cb);

    [DllImport("MyLibrary1")]
    public static extern void InvokeRegisteredCallbackWithState(IntPtr id);
}

呼び出し側ではコールバックからアクセスしたいオブジェクトに対して GCHandle を割り当てて、ToIntPtr でポインターを取得しネイティブ側に渡します。ネイティブ側では必要に応じて保持して置くなどしてコールバックを呼ぶときにポインターをコールバックに渡します。

NativeMethods.RegisterCallbackWithState(MyClass.CallbackWithState);


var state = new MyState();


var handle = GCHandle.Alloc(state);

var ptr = GCHandle.ToIntPtr(handle);



NativeMethods.InvokeRegisteredCallbackWithState(ptr);


handle.Free();

マネージコールバック側ではポインターを受け取って、GCHandle.FromIntPtr で GCHandle を取得し、そこからオブジェクトを取得します。

public static void CallbackWithState(IntPtr state)
{
    
    var objHandle = GCHandle.FromIntPtr(state);
    
    var stateObj = (MyState)objHandle.Target!;
    
}

GCHandle を割り当てている限り、オブジェクトは GC によって回収されることはありません。裏を返せば GCHandle の開放を忘れるとメモリリークの原因になるので注意が必要です。

また GCHandle.ToIntPtr で得られるポインターはオブジェクトの実体をさしているわけではない点にも注意が必要です。これで得られたアドレスにアクセスして何かが取れるわけではないということです。

GCHandle のドキュメントにも以下のように書かれています。

Normal ハンドルは不透明です。つまり、ハンドルを介して含まれるオブジェクトのアドレスを解決することはできません。

いずれにしてもコールバックを経由して何らかのパラメータが渡るように作られていないと取れない解決方法ではありますが、例えば Win32 API の EnumWindows は lParam 引数がそのような役割を果たしているので似たような解決策を提供しているパターンはなくはないと思います。

この実装パターンは .NET ライブラリーの中でも使用されています(例えば System.Net.Http.WinHttpHandler)。

ところで呼び出し先もデリゲートも CLR オブジェクトですので当然 GC によってヒープ上を移動する可能性があります。例えばコールバックを登録するようなパターンを考えるとコールバック登録後に移動してしまうことは十分考えられますが、移動されても問題ないのかという疑問が頭をよぎります。

先に答えを書いておくと「問題ない」です。

デリゲートが移動されないようにするコードを書いている例やガイダンスはなく、状況的には動かされてしまっても問題がなさそうだという予想はできます。ただ、この辺りの挙動については意外と明確に書かれているドキュメントはなく、特に Marshal.GetFunctionPointerForDelegate はポインターを取得するというものですのでなんとなく不安になります。

実際どのような挙動になるのか、これまでのコードをベースに以下のようなコードを用意して実行してみます。


var tmp = new Listobject>();
for (var i = 0; i  10000; i++) { tmp.Add(new byte[64]); }
GC.Collect();
GC.KeepAlive(tmp);


var obj = new MyClass();
NativeMethods.CallbackDelegate d = obj.Callback;


NativeMethods.RegisterCallback(d);


GC.Collect();



NativeMethods.InvokeRegisteredCallback();

GC.KeepAlive(obj);
GC.KeepAlive(d);

このコードを実際に動かすと正常にコールバックは呼び出されます。

これだとヒープ上で移動していないのではないかという可能性があるのでオブジェクトのメモリー上の位置を確認するコードを追加してみます。

 // 呼び出し先オブジェクトとデリゲートを作る
 var obj = new MyClass();
 NativeMethods.CallbackDelegate d = obj.Callback;

+{
+    Console.WriteLine($"obj: Gen:{GC.GetGeneration(obj)}; ptr:{new nuint+* (void**)Unsafe.AsPointer(ref obj)):X}");
+    Console.WriteLine($"d: Gen:{GC.GetGeneration(d)}; ptr:{new nuint(* +void**)Unsafe.AsPointer(ref d)):X}");
+}

 // 登録する
 NativeMethods.RegisterCallback(d);

 // GC を実行することで移動させる
 GC.Collect();
 
+{
+    Console.WriteLine($"obj: Gen:{GC.GetGeneration(obj)}; ptr:{new nuint(* (void**)Unsafe.AsPointer(ref obj)):X}");
+    Console.WriteLine($"d: Gen:{GC.GetGeneration(d)}; ptr:{new nuint(* (void**)Unsafe.AsPointer(ref d)):X}");
+}

 // コールバックを呼び出す
 NativeMethods.InvokeRegisteredCallback();

これを実行すると下記のような出力が得られます。

obj: Gen:0; ptr:26494000028
d: Gen:0; ptr:26494000040
obj: Gen:1; ptr:26494800270
d: Gen:1; ptr:26494800288
MyCallback: Callback invoked!

この結果からオブジェクト自体は確かに移動していることを確認できます。ということは少なくともデリゲートが回収されていなければ移動されても問題ないようです。

オブジェクト/デリゲートが移動されても問題ない理由

オブジェクトが移動しても問題ない理由は Marshal.GetFunctionPointerForDelegate を呼び出してみるとヒントがあります。

 {
     Console.WriteLine($"obj: Gen:{GC.GetGeneration(obj)}; ptr:{new nuint(* (void**)Unsafe.AsPointer(ref obj)):X}");
     Console.WriteLine($"d: Gen:{GC.GetGeneration(d)}; ptr:{new nuint(* (void**)Unsafe.AsPointer(ref d)):X}");
+    Console.WriteLine($"Marshal.GetFunctionPointerForDelegate(d):  {Marshal.GetFunctionPointerForDelegate(d)}");
 }

 // 登録する
 NativeMethods.RegisterCallback(d);

 // GC を実行することで移動させる
 GC.Collect();

 {
     Console.WriteLine($"obj: Gen:{GC.GetGeneration(obj)}; ptr:{new nuint(* (void**)Unsafe.AsPointer(ref obj)):X}");
     Console.WriteLine($"d: Gen:{GC.GetGeneration(d)}; ptr:{new nuint(* (void**)Unsafe.AsPointer(ref d)):X}");
+    Console.WriteLine($"Marshal.GetFunctionPointerForDelegate(d):  {Marshal.GetFunctionPointerForDelegate(d)}");
 }

 // コールバックを呼び出す
 NativeMethods.InvokeRegisteredCallback();

この行を追加して実行した結果は以下のようになります。

obj: Gen:0; ptr:21A4E000028
d: Gen:0; ptr:21A4E000040
Marshal.GetFunctionPointerForDelegate(d): 140733705236516
obj: Gen:1; ptr:21A4E800270
d: Gen:1; ptr:21A4E800288
Marshal.GetFunctionPointerForDelegate(d): 140733705236516
MyCallback: Callback invoked!

デリゲートのヒープ上の位置は変わっていても Marshal.GetFunctionPointerForDelegate で取得したポインターは変わっていないことがわかります。これはデリゲートのインスタンスに対して変化しない1つの関数ポインターが割当たっていると考えられます。

ということでネイティブの知識は乏しいですがデリゲートの秘密を探るべく、 dotnet/runtime の奥地へと足を踏み入れることにします。

まず Marhsal.GetFunctionPointerForDelegate の実装は GetFunctionPointerForDelegateInternal を呼び出しています。

https://github.com/dotnet/runtime/blob/199ae8820492f05169529da1f262e9736007448a/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshal.cs#L1138-L1145

GetFunctionPointerForDelegateInternal の定義を見ると QCall 要するに CoreCLR のネイティブ実装であると定義されています。

https://github.com/dotnet/runtime/blob/199ae8820492f05169529da1f262e9736007448a/src/coreclr/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshal.CoreCLR.cs#L985-L991

GetFunctionPointerForDelegateInternal の実装は C++ のほうにあり、実質的には COMDelegate::ConvertToCallback の呼び出しです。

https://github.com/dotnet/runtime/blob/199ae8820492f05169529da1f262e9736007448a/src/coreclr/vm/marshalnative.cpp#L254

この COMDelegate::ConvertToCallback は何をするものかというと、デリゲートを呼び出す thunk (サンク)コードを必要に応じて生成して、その関数ポインターを返すものです。Thunk はここでは「デリゲートのハンドルを保持して、デリゲートを呼び出す小さなコード」という感じでしょうか。

COMDelegate::ConvertToCallback の thunk を生成する一連の処理の中で CreateLongWeakHandle でデリゲートに対する弱参照を作成して、thunk (UMEntryThunk) を作る際の情報としてもっています。

https://github.com/dotnet/runtime/blob/5d3fe140d7565461e02b515a49f3155106048926/src/coreclr/vm/comdelegate.cpp#L1368-L1378

というわけで Marshal.GetFunctionPointerForDelegate で返ってくる関数ポインターの実体がデリゲートへの(弱)参照を持っていそうなことが分かったので移動しても大丈夫そうな雰囲気が見えてきました。

折角なので実際によびだす実装はどのようになっているのか確認したいところではありますが、これは結構複雑な仕組みになっています。そもそも thunk が作られた最初の時点では実際に直接デリゲートを呼び出すコードを持っているわけではなく「デリゲートを呼び出すコードを生成するためのコード」となっています。このコードが呼びだされると、動的にコードが生成されデリゲートを呼ぶ仕組みです。

つまり「ネイティブ → サンク[スタブ → 実行時 IL 生成 → JIT コンパイル] → デリゲート実行」という流れになっています。この実行時 IL 生成 & JIT のスタブはここに限らずいろいろなところで使われているそうです。詳しくはMatt Warren 氏の “Stubs” in the .NET Runtime で解説されています

実際にネイティブからマネージのコールバックの初回実行時の処理を追いかけると UMEntryThunk::RunTimeInit()UMThunkMarshInfo::RunTimeInit() が呼ばれていることを確認できます。(スクリーンショットはネイティブライブラリーがコールバックを呼んだ際のコールスタック)

https://github.com/dotnet/runtime/blob/34e64ad57037093849bbb60a79666b2a3f3746c2/src/coreclr/vm/dllimportcallback.cpp#L387-L394

ここから GetILStubMethodDesc -> NDirect::CreateCLRToNativeILStub -> CreateInteropILStub -> CreateNDirectStubWorker と進んで、

https://github.com/dotnet/runtime/blob/34e64ad57037093849bbb60a79666b2a3f3746c2/src/coreclr/vm/dllimportcallback.cpp#L334-L357

https://github.com/dotnet/runtime/blob/34e64ad57037093849bbb60a79666b2a3f3746c2/src/coreclr/vm/dllimport.cpp#L5165-L5171

https://github.com/dotnet/runtime/blob/34e64ad57037093849bbb60a79666b2a3f3746c2/src/coreclr/vm/dllimport.cpp#L5216

https://github.com/dotnet/runtime/blob/34e64ad57037093849bbb60a79666b2a3f3746c2/src/coreclr/vm/dllimport.cpp#L3596-L3610

その中で ILStubState (PInvoke_ILStubState) を使って IL を生成します。そして ILSubState::EmitInvokeTarget から NDirectStubLinker::DoNDirect で実際にメソッドを呼び出す IL を生成します。(NDirect は P/Invoke のこと)

https://github.com/dotnet/runtime/blob/0d20f9ad3e0fd58a510062757b34f76a3c122b25/src/coreclr/vm/dllimport.cpp#L2084

この NDirectStubLinker::DoNDirect の中ほどに native-to-managed というコメントがついた else ブロックがあります。ここでスタブに含まれたコンテキスト(これが thunk からデリゲートへのオブジェクトハンドルを取り出し、そこからデリゲートを取り出して、_methodPtrを取得して、calli でメソッドを呼び出しという一連の IL を生成しています。

https://github.com/dotnet/runtime/blob/0d20f9ad3e0fd58a510062757b34f76a3c122b25/src/coreclr/vm/dllimport.cpp#L2137-L2149

そして最後に UMThunkMarshInfo::RunTimeInit() で生成した IL を JIT にかけて、出来上がったコードで ILStub を差し替えて完了です。

https://github.com/dotnet/runtime/blob/0d20f9ad3e0fd58a510062757b34f76a3c122b25/src/coreclr/vm/dllimportcallback.cpp#L433-L436

この後は「ネイティブ → サンク[JIT コンパイルされたコード] → デリゲート実行」という形で実行されます。

というわけですべての処理を細かく追えているわけではないですが、大体の流れとデリゲートが移動されても大丈夫な理由がなんとなくわかったような気がしました。

ところで生成されている IL は OBJECTHANDLE からデリゲートを取得して呼び出していて、呼べるものなのかという疑問が浮かんできました。内部でのハンドルの取り扱い的には弱参照なのか強参照(ref)なのかはハンドルからオブジェクトを取得する分には関係なさそうには見えたのですが、折角なので少し試してみます。

var d = () => Console.WriteLine("Hello");







var weakRefForDelegate = new WeakReference(d);



var handle = GetInternalTaggedHandle(weakRefForDelegate);


var dynamicMethod = new DynamicMethod("CallHandle", typeof(void), [typeof(nint)]);
var field_methodPtr = typeof(Delegate).GetField("_methodPtr", BindingFlags.NonPublic | BindingFlags.Instance);
var il = dynamicMethod.GetILGenerator();
{
    il.Emit(OpCodes.Ldarg_0); 
    il.Emit(OpCodes.Ldind_Ref); 
    il.Emit(OpCodes.Ldfld, field_methodPtr); 
    il.EmitCalli(OpCodes.Calli, CallingConventions.Standard, typeof(void), [], []);
    il.Emit(OpCodes.Ret);
}
var callDelegate = dynamicMethod.CreateDelegateActionnint>>();
callDelegate(handle);

[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_taggedHandle")]
static extern ref nint GetInternalTaggedHandle(WeakReference wr);

これを実行すると特にエラー無く Hello と表示されるので、特に区別はなさそうです。ref 引数で渡した参照型の呼び出しは ldind.ref になるので WeakReference から得られたハンドルを ref 引数に無理やり渡しても動きそうです(どう考えても危険ですが)。

フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -

Most Popular