Re:ゼロアロケーションから始めるP/Invoke

# はじめに
皆さん、黒魔術楽しんでますか？
今回はWINAPIなどの外部DLLを、今風にゼロアロケーションで呼び出す黒魔術を書いていきたいと思います。

例として呼び出すWINAPIは、コンピュータ名を取得する`GetComputerNameW`をターゲットにします。

# `GetComputerNameW`のシグネチャ

https://www.pinvoke.net/default.aspx/kernel32.getcomputername

pinvoke.netの`GetComputerNameEx`を参考に`GetComputerNameW`のpinvokeを書くと以下のようになります。

```csharp:シグネチャ
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern bool GetComputerNameW(StringBuilder lpBuffer, ref uint lpnSize);
```

使い方はこんな感じです。

```csharp:使い方
uint size = 256;
var sb = new StringBuilder((int)size);

GetComputerNameW(sb, ref size);

var name = sb.ToString();
```

見ただけで黒魔術使い（見習い）としては、とてもムズムズします！

# 何が良くない？
`StringBuilder`を結果の受け口として利用しているため、アロケーションが発生してしまいます。全然今風ではないですね！

しかも、結果としてコンピュータ名を取り出すために`ToString`でさらにアロケーションが発生してしまいます。

なので、`StringBuilder`を抹殺するために、最近流行の黒魔術`Span<T>`と`stackalloc`を使います。

# `StringBuilder`を抹殺する
まずは先ほどの`GetComputerNameW`のシグネチャを変更します。

```csharp:シグネチャ（変更後）
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern bool GetComputerNameW(ref char buffer, ref uint lpnSize);
```

第一引数を`StringBuilder`から`ref char`に変更しました。

```csharp:使い方（変更後）
Span<char> buffer = stackalloc char[256];
var size = (uint)buffer.Length;

GetComputerNameW(ref MemoryMarshal.GetReference(buffer), ref size);

var name = buffer[..(int)size].ToString();
```

これで`StringBuilder`は抹殺できました！`stackalloc`を使ったのでアロケーションも1回分減っています！

でも`buffer`を`ToString`しているため、ここでアロケーションが発生してしまいます。

もし、コンピュータ名を調べて特定の名前かどうかをチェックするとしたら、`ToString`も抹殺可能です！

```csharp:ToStringの抹殺
Span<char> buffer = stackalloc char[256];
var size = (uint)buffer.Length;

GetComputerNameW(ref MemoryMarshal.GetReference(buffer), ref size);

var targetNameSpan = "Hoge".AsSpan();
if(MemoryExtensions.SequenceEqual(buffer[..(int)size], targetNameSpan))
{
    // コンピュータ名は Hoge
}
```
`MemoryExtensions.SequenceEqual`は`Span<T>`同士が一致しているかを調べてくれます。この関数は内部で恐ろしいほどの最適化が加えられているため、ループで比較するよりもめちゃくちゃ速いです！絶対に使用しましょう！

詳しく知りたい方は以下の記事がすごく勉強になります。

https://qiita.com/Kujiro/items/e687b089a0348d989f1b

# 関数化してみる

```csharp:完全体
public static uint GetComputerName(Span<char> buffer)
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern bool GetComputerNameW(ref char buffer, ref uint lpnSize);

    var size = (uint)buffer.Length;
    GetComputerNameW(ref MemoryMarshal.GetReference(buffer), ref size);
    return size;
}
```

ローカル関数を使ってシグネチャを関数スコープに閉じ込めるのがマイブームです！

```csharp:使い方
Span<char> buffer = stackalloc char[256];
var size = GetComputerName(buffer);
var str = buffer[..(int)size].ToString();
```

# まとめ
これでゼロアロケーションでWINAPIを高速に詠唱することができるようになりました！

めでたしめでたし！

.
.
.

ではないんです！まだやれることがあります！

# 詠唱を`SkipLocalsInit`を使って無詠唱にする！
`SkipLocalsInit`とはなんぞや？という方も多いと思います。簡単に言うと、宣言された変数を0で初期化するのを止めてくれます。

以下のサイトで詳しく説明してくれています。

https://www.meziantou.net/csharp-9-improve-performance-using-skiplocalsinit.htm

今回の場合、`GetComputerName`を呼び出す際に`stackalloc`していますが、デフォルトの動作として確保した領域を0初期化しています。どうせWINAPI側で上書きするのだからすごく無駄です。

使い方は関数に`SkipLocalsInit`属性をつけてあげればOKです

```csharp:SkipLocalsInitの使い方
public static uint GetComputerName(Span<char> buffer)
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern bool GetComputerNameW(ref char buffer, ref uint lpnSize);

    var size = (uint)buffer.Length;
    GetComputerNameW(ref MemoryMarshal.GetReference(buffer), ref size);
    return size;
}

[SkipLocalsInit]
public static void Call()
{
    Span<char> buffer = stackalloc char[256];
    var size = GetComputerName(buffer);
    var str = buffer[..(int)size].ToString();
}
```

すごくいい感じでゾクゾクします！
ただし、`SkipLocalsInit`はコンパイルオプションに`/unsafe`が必要となります。
黒魔術使いなら当然ONになっているはずなので問題ないですね！