LoginSignup
17
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-02

本記事はサムザップ #1 AdventCalendar 2020の12/2の記事です。
12/1の記事は@ohbashunsukeさんの【Unity】新規ゲームのUI開発で気をつけた39のTips前編 - Qiitaでした。

モチベーション

P/Invokeと各種String作成ベンチマーク - Qiitaの記事で紹介されているベンチマークでは、
StringBuilderは他の手法に比べて頭一つ遅くなっていました。

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

準備

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

$ dotnet new console

ネイティブ関数呼び出しの実装(Program.cs)
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());
        }
    }
}

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

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

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

調査

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

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

IL DASM 出力結果

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
{
}

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

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

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

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

$ 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

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

perfview-events.png

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

perfview-stubmethod-detail.png

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

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

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

参考

17
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
2