はじめに
皆さん、黒魔術楽しんでますか?
今回はWINAPIなどの外部DLLを、今風にゼロアロケーションで呼び出す黒魔術を書いていきたいと思います。
例として呼び出すWINAPIは、コンピュータ名を取得するGetComputerNameW
をターゲットにします。
GetComputerNameW
のシグネチャ
pinvoke.netのGetComputerNameEx
を参考にGetComputerNameW
のpinvokeを書くと以下のようになります。
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern bool GetComputerNameW(StringBuilder lpBuffer, ref uint lpnSize);
使い方はこんな感じです。
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
のシグネチャを変更します。
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern bool GetComputerNameW(ref char buffer, ref uint lpnSize);
第一引数をStringBuilder
からref char
に変更しました。
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
も抹殺可能です!
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>
同士が一致しているかを調べてくれます。この関数は内部で恐ろしいほどの最適化が加えられているため、ループで比較するよりもめちゃくちゃ速いです!絶対に使用しましょう!
詳しく知りたい方は以下の記事がすごく勉強になります。
関数化してみる
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;
}
ローカル関数を使ってシグネチャを関数スコープに閉じ込めるのがマイブームです!
Span<char> buffer = stackalloc char[256];
var size = GetComputerName(buffer);
var str = buffer[..(int)size].ToString();
まとめ
これでゼロアロケーションでWINAPIを高速に詠唱することができるようになりました!
めでたしめでたし!
.
.
.
ではないんです!まだやれることがあります!
詠唱をSkipLocalsInit
を使って無詠唱にする!
SkipLocalsInit
とはなんぞや?という方も多いと思います。簡単に言うと、宣言された変数を0で初期化するのを止めてくれます。
以下のサイトで詳しく説明してくれています。
今回の場合、GetComputerName
を呼び出す際にstackalloc
していますが、デフォルトの動作として確保した領域を0初期化しています。どうせWINAPI側で上書きするのだからすごく無駄です。
使い方は関数にSkipLocalsInit
属性をつけてあげればOKです
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になっているはずなので問題ないですね!