はじめに
皆さん、黒魔術楽しんでますか?
今回は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になっているはずなので問題ないですね!