【C#】Unsafeクラスで安全で危険なコードを書こう()

このエントリーをはてなブックマークに追加

unsafeとは(哲学)

C#でのunsafe

C#では元来、危険であると書き手が分かっていれば危険でもパフォーマンスを重視した書き方(ポインター)ができました。 その危険という状態を表すのがunsafeコンテキストです。

個人的にはこの思想は気に入っていて、ポインターを理解してる人が書いたコードの恩恵をポインターを理解できてない人にも伝搬できる素晴らしいものです(ポインターを理解してないならsafeコンテキストだけ扱えばいい、unsafeコンテキストのことは理解してる人に任せるということ)。

Unsafeクラス

System.Runtime.CompilerServices.Unsafeを使うとC#的に痒いところに手が届くようになります。

そこで問題なのが、このUnsafeクラスはsafeコンテキストでも扱えるものがあったりするということ。そしてこのクラスのソースコードはCILということ……

基本的にsafeコンテキストで使えようがUnsafeクラスの名前の通りunsafeなのでほいほい使うような品物ではありませんが、どういう機能があるのか知らないと適切な使いどころで使えなくなってしまうので、少しだけ調べてみました。

ソースコードはgistにおいてます。

Unsafe.Add

var ar = new int[] { 0, 1, 2, 3, 4, 5 };
int i3 = Unsafe.Add(ref ar[0], 3);
Console.WriteLine($"0..5's offset 3 is {i3}"); // 3

第1引数のアドレスに第2引数を足した結果を返すという品物です。引いた結果を返すSubtractというメソッドもあるようです。 上述のコードでは値を返却させてますがref int i3 = ref Unsafe.Add(ref ar[0], 3);とすれば参照を取ることもできます。

.method public hidebysig static !!T& Add<T>(!!T& source, int32 elementOffset) cil managed aggressiveinlining
{
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = ( 01 00 00 00 )
    .maxstack 3
    ldarg.0
    ldarg.1
    sizeof !!T
    conv.i
    mul
    add
    ret
}

ソースコードはこのようになっていて、C#的に記述するとsource + elementOffset * sizeof(T)のようなことをしています。

Unsafe.AreSame

var a = new Object();
var b = new Object();
ref object ra1 = ref a;
ref object ra2 = ref a;
ref object rb = ref b;
Console.WriteLine($"{nameof(ra1)} and {nameof(ra2)} areSame: {Unsafe.AreSame(ref ra1, ref ra2)}"); // true
Console.WriteLine($"{nameof(ra1)} and {nameof(rb)} areSame: {Unsafe.AreSame(ref ra1, ref rb)}"); // false

ぱっと見ReferenceEqualsみたいな感じですかね。

.method public hidebysig static bool AreSame<T>(!!T& left, !!T& right) cil managed aggressiveinlining
{
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = ( 01 00 00 00 )
    .maxstack 2
    ldarg.0
    ldarg.1
    ceq
    ret
}

ソースコードはこのようになっていてC#的に記述するとleft == rightのようなわかりやすいものになっています。(使いどころがよくわからない)

Unsafe.As

object a = "object";
ref object ra = ref a;
string ca = Unsafe.As<string>(a);
string cra = Unsafe.As<object, string>(ref ra);
Console.WriteLine($"{nameof(ca)}: {ca}, {nameof(cra)}: {cra}"); // object, object

見た目そのまま型の変換に使えます。 ソースコードはこちらにありますが、ちょっと危険なコードでもあります。

こちら(アジョブジ星通信 -さぁ fixed を捨てて Unsafe だ)で面白い使い方を紹介されてます。

Unsafe.AsPointer, Unsafe.AsRef

var ar = new int[] { 0, 1, 2, 3, 4, 5 };
void* pointer = Unsafe.AsPointer(ref ar[0]);
int* intptr = (int*)pointer;
Console.WriteLine($"*({nameof(intptr)}+1) = {*(intptr + 1)}"); // 1
var ar = new int[] { 0, 1, 2, 3, 4, 5 };
fixed (int* intptr = &ar[0])
{
    void* pointer = (void*)(intptr + 4);
    int i4 = Unsafe.AsRef<int>(pointer);
    Console.WriteLine($"{nameof(intptr)} + 4 = {i4}"); // 4
}

これらは参照とポインターの相互変換で、CIL的にも変換のコードしか書かれてません。 ソースはここここ

Unsafe.CopyBlock

var ar1 = new byte[] { 0, 1, 2, 3, 4, 5 };
var ar2 = new byte[ar1.Length];
Unsafe.CopyBlock(ref ar2[0], ref ar1[0], (uint)ar1.Length);
Console.WriteLine("Copied");
foreach (byte b in ar2)
{
    Console.WriteLine(b); // 0, 1, 2, 3, 4, 5
}

バイト配列をコピーするメソッドです。

.method public hidebysig static void CopyBlock(uint8& destination, uint8& source, uint32 byteCount) cil managed aggressiveinlining
{
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = ( 01 00 00 00 )
    .maxstack 3
    ldarg.0
    ldarg.1
    ldarg.2
    cpblk
    ret
}

ソースコードはこんな感じで、cpblkというのが肝心の命令ですね。 お察しのとおりメモリブロックをコピーする命令です。(ブロックと言ってもunsigned int8しかできなさそうですが)

使いどころはそのうち遭遇しそうです。

Unsafe.InitBlock

var ar = new byte[6];
Unsafe.InitBlock(ref ar[0], 3, (uint)ar.Length);
Console.WriteLine("Init");
foreach (byte b in ar)
{
    Console.WriteLine(b); // 3, 3, 3, 3, 3, 3
}

配列の初期値を0以外で確保したいときとかに使えそうです。

.method public hidebysig static void InitBlock(uint8& startAddress, uint8 'value', uint32 byteCount) cil managed aggressiveinlining
{
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = ( 01 00 00 00 )
    .maxstack 3
    ldarg.0
    ldarg.1
    ldarg.2
    initblk
    ret
}

ソースコードはこんな感じで、これも肝心な命令はinitblkです。cpblkと同じようなもので、メモリブロックを第2引数の値で埋めるものです。 (これはそのうち使い道に遭遇しそう)

Unsafe.Read

int a = 10;
int* intptr = &a;
void* pointer = (void*)intptr;
Console.WriteLine($"Read intptr: {Unsafe.Read<int>(pointer)}"); // 10

見た目そのままポインターの値を読み取るものです。書き込みもWriteメソッドとして用意されてますが、使いどころがいまいち……

ソースコードはここです。

Unsafe.SizeOf

Console.WriteLine($"string size: {Unsafe.SizeOf<string>()}"); // 8 (in 64bit OS)
.method public hidebysig static int32 SizeOf<T>() cil managed aggressiveinlining
{
    .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = ( 01 00 00 00 )
    .maxstack 1
    sizeof !!T
    ret
}

ソースコードを見ればわかりますが単純にCILのsizeof命令を呼び出してるだけです。参照型は32bitOSか64bitOSかで4か8を返すことになると思います。(たぶん)

他にも

めんどくさくて紹介しなかったメソッドもありますし、ドキュメントのほうは .NET Core 2.0の情報までしか現時点では載っていないようですが、IsAddressGreaterThanとかIsAddressLessThanとかUnboxとかが追加されてるようです。 これらはnugetで配られてるパッケージを利用すれば .NET Standard 2.0をサポートしている環境なら使えるかと思います。

まとめ

unsafeとは(哲学)


途中から力尽きた感じになってスマンかった