C#のネイティブ関数呼び出し（P/Invoke）時に行われていることを調べてみた

本記事は[サムザップ #1 AdventCalendar 2020](https://qiita.com/advent-calendar/2020/sumzap1)の12/2の記事です。
12/1の記事は@ohbashunsukeさんの[【Unity】新規ゲームのUI開発で気をつけた39のTips前編 - Qiita](https://qiita.com/ohbashunsuke/items/0570234362e2e45c0011)でした。

## モチベーション

[P/Invokeと各種String作成ベンチマーク - Qiita](https://qiita.com/KageShiron/items/8093a68e824e1f6c8300)の記事で紹介されているベンチマークでは、
StringBuilderは他の手法に比べて頭一つ遅くなっていました。

他と比べて遅いのはマネージド・ネイティブ間のデータの受け渡し（マーシャリング）コストが上乗せされた結果のような感じがしますが、
実際に何が行われているのか、気になったので調べてみました。

## 準備

まずはネイティブ関数呼び出しを行う、ミニマムなコンソールアプリケーションを作成します。

```
$ dotnet new console
```

<details><summary>ネイティブ関数呼び出しの実装(`Program.cs`)</summary><div>

```Program.cs
using System;
using System.Runtime.InteropServices;
using System.Text;

namespace pinvoke
{
    internal class Program
    {
        [DllImport("Kernel32", CharSet = CharSet.Unicode, EntryPoint = "GetTempPathW")]
        public static extern int GetTempPath(uint nBufferLength, StringBuilder sb);

        public static void Main(string[] args)
        {
            var sb = new StringBuilder(capacity: 260 + 1);
            GetTempPath(260, sb);
            Console.WriteLine(sb.ToString());
        }
    }
}
```

</div></details>

動作確認のためにコードをビルドして実行します。

```
$ dotnet build
$ dotnet run
C:\Users\user\AppData\Local\Temp\
```

想定通りに一時ディレクトリのパスが出力されました、ネイティブ関数呼び出しの実装は問題なさそうです。

## 調査

C#ではMashalAs属性を使って、ネイティブ関数呼び出し時のデータの受け渡し方法をコントロールすることができます。
設定によって文字コードの変換（ANSI → UTF-16など）もかかる、これらのマーシャリングを行うコードはいつ作られるのでしょうか。

まずコンパイル時の可能性を考えますが、これはビルドしたDLLをIL DASMで見てみてもC#のコードとほぼ変わらないため、違うことが分かります。

<details><summary>IL DASM 出力結果</summary><div>

Mainメソッドからの`GetTempPath`関数呼び出しは、普通の関数呼び出しと変わらない

```
  IL_0006:  newobj     instance void [System.Runtime]System.Text.StringBuilder::.ctor(int32)
  IL_000b:  stloc.0
  IL_000c:  ldc.i4     0x104
  IL_0011:  ldloc.0
  IL_0012:  call       int32 pinvoke.Program::GetTempPath(uint32,
                                                          class [System.Runtime]System.Text.StringBuilder)
```

`GetTempPath`のメソッド本体は空になっている

```
.method public hidebysig static pinvokeimpl("Kernel32" as "GetTempPathW" unicode winapi) 
        int32  GetTempPath(uint32 nBufferLength,
                           class [System.Runtime]System.Text.StringBuilder sb) cil managed preservesig
{
}
```

</div></details>

ではコンパイル時ではないとすると、コードが生成されるのは実行時になるのでしょうか。
結論から言うとそうなります。

こちらの記事[^1]によるとランタイム（CLR）によって実行時にマーシャリングのコードが生成されること、
また、その生成されるコードの詳細をETWイベント経由で確認できるツールが紹介されています。

[^1]: [Improvements to Interop Marshaling in V4: IL Stubs Everywhere | .NET Blog](https://devblogs.microsoft.com/dotnet/improvements-to-interop-marshaling-in-v4-il-stubs-everywhere/)

このIL Stub Diagnosticsというツール [^2] ですが、残念ながら私のPCでは動作しなかったので、
直接実行時のETWイベントから詳細を取得してみることにします。

[^2]: [IL Stub Diagnostic Tool | .NET Blog](https://devblogs.microsoft.com/dotnet/il-stub-diagnostic-tool/)

ドキュメント[^3]を参照すると`dotnet-trace`コマンドでプロバイダーを`Microsoft-Windows-DotNETRuntime`に、
フラグを`0x2000`、レベルを`4`にすると、スタブ生成のETWイベントが捕捉できそうです。

[^3]: [Interop runtime events - .NET | Microsoft Docs](https://docs.microsoft.com/en-us/dotnet/fundamentals/diagnostics/runtime-interop-events)

```
$ dotnet-trace collect --providers Microsoft-Windows-DotNETRuntime:0x2000:4 -- ./bin/Debug/net5.0/pinvoke.exe
Provider Name                           Keywords            Level               Enabled By
Microsoft-Windows-DotNETRuntime         0x0000000000002000  Informational(4)    --providers

Process        : C:\Users\user\...\pinvoke\bin\Debug\net5.0\pinvoke.exe
Output File    : C:\Users\user\...\pinvoke\trace.nettrace

[00:00:00:00]   Recording trace 0.00     (B)
Press <Enter> or <Ctrl+C> to exit...

Trace completed.
```

トレースが完了したら `trace.nettrace` というファイルが出力されますので、これをPerfViewで開いてみます。

![perfview-trace_nettrace.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/3a3f5b89-9fee-f8c3-9f07-e943ad39b116.png)


`trace.nettrace`のEventsをダブルクリックすると、新しいウィンドウでETWイベントの一覧が表示されます。

![perfview-events.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/093a5d22-e4e8-efeb-851b-f48078e0a1fa.png)

今回の実装に使った`GetTempPath`でフィルタリングすると、確かにILコードがありました。

![perfview-stubmethod-detail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/201332/27f52a08-2e86-78b2-47f0-f751afaf1f90.png)


ILにはあまり詳しくないので雰囲気で読み解いていくと、
スタブコードのネイティブ関数呼び出し前後で呼ばれてそうなメソッドは下記でした。

コードには*条件分岐*も含まれていましたので正確ではありませんが、

```cs
Marshal.AllocCoTaskMem
StringBuilder.InternalCopy
// ネイティブ関数呼び出し
StringBuilder.ReplaceBufferInternal
Marshal.FreeCoTaskMem
```

メソッド名から察するに、一時バッファを用意しそこにStringBuilderの内容をコピーし、
ネイティブ関数呼び出し後書き戻して割り当てたバッファを解放する、みたいな挙動でしょうか。

なるほど他の手法に比べて遅くなりそうな雰囲気がします。

## まとめ

- .NET Coreではネイテイブ関数呼び出し時に、マネージドとネイティブの橋渡し(マーシャリング)をするスタブコードを生成している
- スタブコードは生成のETWイベントにより、その詳細を知ることができる

以上になります。
明日は@phasmatodeanさんの記事です。

## 環境
Windows 10 Pro 20H2

```
$ dotnet --version
5.0.100

$ dotnet-trace --version
5.0.152202+4d281c71a14e6226ab0bf0c98687db4a5c4217e3
```

## 参考
- [PInvoke: beyond the magic | devops.lol](https://www.devops.lol/pinvoke-beyond-the-magic/)
- [dotnet-trace 診断ツール - .NET CLI | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/core/diagnostics/dotnet-trace)