LoginSignup
6
3

More than 3 years have passed since last update.

[C#] Unsafeクラスの各メソッドを簡易解説(逆引き)

Last updated at Posted at 2020-07-12

System.Runtime.CompilerServices.Unsafe クラスは一部で大変便利なのですが、ドキュメントを見ても動作が分かりにくいことが多いので、実際のILとC#へ逆コンパイルした結果を併記して簡易解説(メモ)します。

  • 本質的でない属性は除去しています。
    例: .custom instance void System.Runtime.Versioning.NonVersionableAttribute::.ctor() = ( 01 00 00 00 )
  • ニュアンスをつかみやすくするため、ILSpy version 6.0.0.5830-rc1 での逆コンパイル結果を併記しています。
  • 逆コンパイル結果がおかしい場合がありますが、ILをC#的に表現できないものがあるので気にしません。
  • ILコードをリポジトリから丸コピーしているので、ライセンスはThe MIT License (MIT)になります。

ILについて

  • .で始まるものはディレクティブ
  • &はそのまま「参照」。C++/CLIでいうインテリア参照
  • !!Tは、Tという名前のメソッドのジェネリック型引数です。
  • native intIntPtrです。
  • スタックベースの仮想マシンです。引数はスタックに積まれています。この記事で頻出の命令をメモしておきます。
    • 関数の引数は右側を先にスタックに積みます。
    • ldarg.Nで引数のN番目をスタックにロードします。
    • .maxstack N で使用する最大のスタック数を宣言します。
    • cpblk スタックから、コピー先、コピー元、バイト数をポップして、コピーする。コピー元とコピー先の領域とが重なっている場合,cpblkの動作は未規定。
  • 詳しくはWikipediaのリストPDFを見てください。
用語 この記事での使い方
ref IL的にはマネージドポインターですが、長いのでC#のrefとして表記します。
ポインター 「マネージドポインター」以外は、アンマネージドポインターとして用います。
オブジェクト参照 参照型のインスタンス
アライメントされた アーキテクチャに依存するアドレスに配置された

参照・ポインターの再解釈系

基本的にldarg.0してretする、つまり引数をそのまま戻り値の型として呼び出し元に返すだけ。

ジェネリック型のrefをvoidポインターとして解釈する

conv.u で参照をポインターに変換しているっぽい。

public unsafe static void* AsPointer<T>(ref T value)
{
    return Unsafe.AsPointer(ref value);
}
.method public hidebysig static void* AsPointer<T>(!!T& 'value') cil managed aggressiveinlining
{
    .maxstack 1
    ldarg.0
    conv.u
    ret
} // end of method Unsafe::AsPointer

型チェックをスキップして、object型を参照型のジェネリック型に再解釈する

純粋に再解釈するので誤ったキャストも許容される。
キャストの場合は、unbox.any !!T命令が使用される。

public static T As<T>(object o) where T : class
{
    return (T)o;
}
.method public hidebysig static !!T As<class T>(object o) cil managed aggressiveinlining
{
    .maxstack 1
    ldarg.0
    ret
} // end of method Unsafe::As

あるジェネリック型のrefを、別のジェネリック型のrefに再解釈する

C++でいうとTFrom& source = ...; return (TTo&)source; といったイメージ。

public static ref TTo As<TFrom, TTo>(ref TFrom source)
{
    return ref Unsafe.As<TFrom, TTo>(ref source);
}
.method public hidebysig static !!TTo& As<TFrom, TTo>(!!TFrom& source) cil managed aggressiveinlining
{
    .maxstack 1
    ldarg.0
    ret
} // end of method Unsafe::As

voidポインターをジェネリック型のrefに再解釈する

public unsafe static ref T AsRef<T>(void* source)
{
    return ref *(T*)source;
}
.method public hidebysig static !!T& AsRef<T>(void* source) cil managed aggressiveinlining
{
// For .NET Core the roundtrip via a local is no longer needed see:
// https://github.com/dotnet/coreclr/issues/13341
// and
// https://github.com/dotnet/coreclr/pull/11218
#ifdef netcoreapp
    .maxstack 1
    ldarg.0
    ret
#else
    .locals (int32&)
    .maxstack 1
    ldarg.0
    // Roundtrip via a local to avoid type mismatch on return that the JIT inliner chokes on.
    stloc.0
    ldloc.0
    ret
#endif
} // end of method Unsafe::AsRef

readonly refをジェネリック型のrefに再解釈する

public static ref T AsRef<T>(in T source)
{
    return ref source;
}

.param [1] で仮引数1(1オリジン)に属性を適用している。

.method public hidebysig static !!T& AsRef<T>(!!T& source) cil managed aggressiveinlining
{
    .param [1]
#ifdef netcoreapp
    .custom instance void [CORE_ASSEMBLY]System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 )
#else
    .custom instance void System.Runtime.CompilerServices.IsReadOnlyAttribute::.ctor() = ( 01 00 00 00 )
#endif
    .maxstack 1
    ldarg.0
    ret
} // end of method Unsafe::AsRef

コピー/読み取り/書き込み系

アラインメントされたvoidポインターから、読んだ値を返す

Cでいうと*((T*)source);

public unsafe static T Read<T>(void* source)
{
    return *(T*)source;
}
.method public hidebysig static !!T Read<T>(void* source) cil managed aggressiveinlining
{
    .maxstack 1
    ldarg.0
    ldobj !!T
    ret
} // end of method Unsafe::Read

アラインメントされていないポインタから、読んだ値を返す

IA(x86, x64アーキテクチャ)では、恐らくUnalignedが付かない方を使用しても、(性能以外で)ペナルティはない。
逆コンパイル結果はアライメントされたものと同じだが、C#で表現できないからだろう。

public unsafe static T ReadUnaligned<T>(void* source)
{
    return *(T*)source;
}
.method public hidebysig static !!T ReadUnaligned<T>(void* source) cil managed aggressiveinlining
{
    .maxstack 1
    ldarg.0        
    unaligned. 0x1
    ldobj !!T
    ret
} // end of method Unsafe::ReadUnaligned

アラインメントされていないbyte型のマネージドポインターから、読んだ値を返す

逆コンパイル結果はアライメントされたものと同じだが、C#で表現できないからだろう。

public unsafe static T ReadUnaligned<T>(ref byte source)
{
    return *(T*)(&source);
}
.method public hidebysig static !!T ReadUnaligned<T>(uint8& source) cil managed aggressiveinlining
{
    .maxstack 1
    ldarg.0        
    unaligned. 0x1
    ldobj !!T
    ret
} // end of method Unsafe::ReadUnaligned

アラインメントされたvoidポインターへ値を書き込む

Cでいうと*((T*)) = value;

public unsafe static void Write<T>(void* destination, T value)
{
    *(T*)destination = value;
}
.method public hidebysig static void Write<T>(void* destination,
                                                !!T 'value') cil managed aggressiveinlining
{
    .maxstack 2
    ldarg.0
    ldarg.1
    stobj !!T
    ret
} // end of method Unsafe::Write

アラインメントされていないvoidポインターへ値を書き込む

Cでいうと*((__unaligned T*)) = value;

public unsafe static void WriteUnaligned<T>(void* destination, T value)
{
    *(T*)destination = value;
}
.method public hidebysig static void WriteUnaligned<T>(void* destination,
                                                !!T 'value') cil managed aggressiveinlining
{
    .maxstack 2
    ldarg.0
    ldarg.1
    unaligned. 0x01
    stobj !!T
    ret
} // end of method Unsafe::WriteUnaligned

アラインメントされていないbyte型のrefへ値を書き込む

public unsafe static void WriteUnaligned<T>(ref byte destination, T value)
{
    *(T*)(&destination) = value;
}
.method public hidebysig static void WriteUnaligned<T>(uint8& destination,
                                                !!T 'value') cil managed aggressiveinlining
{
    .maxstack 2
    ldarg.0
    ldarg.1        
    unaligned. 0x01
    stobj !!T
    ret
} // end of method Unsafe::WriteUnaligned

ジェネリック型のrefからvoidポインターへコピーする

public unsafe static void Copy<T>(void* destination, ref T source)
{
    *(T*)destination = source;
}
.method public hidebysig static void Copy<T>(void* destination,
                                            !!T& source) cil managed aggressiveinlining
{
    .maxstack 2
    ldarg.0
    ldarg.1
    ldobj !!T
    stobj !!T
    ret
} // end of method Unsafe::Copy

voidポインターからジェネリック型のrefへコピーする

public unsafe static void Copy<T>(ref T destination, void* source)
{
    destination = *(T*)source;
}
.method public hidebysig static void Copy<T>(!!T& destination,
                                            void* source) cil managed aggressiveinlining
{
    .maxstack 2
    ldarg.0
    ldarg.1
    ldobj !!T
    stobj !!T
    ret
} // end of method Unsafe::Copy

voidポインターから指定されたバイト数分、voidポインターへコピーする(Cで云うmemcpy)

public unsafe static void CopyBlock(void* destination, void* source, uint byteCount)
{
    // IL cpblk instruction
    Unsafe.CopyBlock(destination, source, byteCount);
}
.method public hidebysig static void CopyBlock(void* destination, void* source, uint32 byteCount) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    ldarg.2
    cpblk
    ret
} // end of method Unsafe::CopyBlock

byte型のrefから指定されたバイト数分、byte型のrefへコピーする(Cで云うmemcpy)

public static void CopyBlock(ref byte destination, ref byte source, uint byteCount)
{
    // IL cpblk instruction
    Unsafe.CopyBlock(ref destination, ref source, byteCount);
}
.method public hidebysig static void CopyBlock(uint8& destination, uint8& source, uint32 byteCount) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    ldarg.2
    cpblk
    ret
} // end of method Unsafe::CopyBlock

アラインメントされていないvoidポインターから指定されたバイト数分、voidポインターへコピーする

public unsafe static void CopyBlockUnaligned(void* destination, void* source, uint byteCount)
{
    // IL cpblk instruction
    Unsafe.CopyBlockUnaligned(destination, source, byteCount);
}
.method public hidebysig static void CopyBlockUnaligned(void* destination, void* source, uint32 byteCount) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    ldarg.2
    unaligned. 0x1
    cpblk
    ret
} // end of method Unsafe::CopyBlockUnaligned

アラインメントされていないbyte型のrefから指定されたバイト数分、voidポインターへコピーする

public static void CopyBlockUnaligned(ref byte destination, ref byte source, uint byteCount)
{
    // IL cpblk instruction
    Unsafe.CopyBlockUnaligned(ref destination, ref source, byteCount);
}
.method public hidebysig static void CopyBlockUnaligned(uint8& destination, uint8& source, uint32 byteCount) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    ldarg.2
    unaligned. 0x1
    cpblk
    ret
} // end of method Unsafe::CopyBlockUnaligned

voidポインターへ指定されたバイト数分、バイト値を書き込む(Cで云うmemset)

public unsafe static void InitBlock(void* startAddress, byte value, uint byteCount)
{
    // IL initblk instruction
    Unsafe.InitBlock(startAddress, value, byteCount);
}
.method public hidebysig static void InitBlock(void* startAddress, uint8 'value', uint32 byteCount) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    ldarg.2
    initblk
    ret
} // end of method Unsafe::InitBlock

byte型のrefへ指定されたバイト数分、バイト値を書き込む(Cで云うmemset)

public static void InitBlock(ref byte startAddress, byte value, uint byteCount)
{
    // IL initblk instruction
    Unsafe.InitBlock(ref startAddress, value, byteCount);
}
.method public hidebysig static void InitBlock(uint8& startAddress, uint8 'value', uint32 byteCount) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    ldarg.2
    initblk
    ret
} // end of method Unsafe::InitBlock

アライメントされていないvoidポインターへ指定されたバイト数分、バイト値を書き込む(Cで云うmemset)

public unsafe static void InitBlockUnaligned(void* startAddress, byte value, uint byteCount)
{
    // IL initblk instruction
    Unsafe.InitBlockUnaligned(startAddress, value, byteCount);
}
.method public hidebysig static void InitBlockUnaligned(void* startAddress, uint8 'value', uint32 byteCount) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    ldarg.2
    unaligned. 0x1
    initblk
    ret
} // end of method Unsafe::InitBlockUnaligned

アライメントされていないbyte型のrefへ指定されたバイト数分、バイト値を書き込む(Cで云うmemset)

public static void InitBlockUnaligned(ref byte startAddress, byte value, uint byteCount)
{
    // IL initblk instruction
    Unsafe.InitBlockUnaligned(ref startAddress, value, byteCount);
}
.method public hidebysig static void InitBlockUnaligned(uint8& startAddress, uint8 'value', uint32 byteCount) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    ldarg.2
    unaligned. 0x1
    initblk
    ret
} // end of method Unsafe::InitBlockUnaligned

アドレス演算系

ジェネリック型のrefを要素数分の加算する

C++で云う(T&)(((T*)&source)+elementOffset)

public static ref T Add<T>(ref T source, int elementOffset)
{
    return ref Unsafe.Add(ref source, elementOffset);
}
.method public hidebysig static !!T& Add<T>(!!T& source, int32 elementOffset) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    sizeof !!T
    conv.i
    mul
    add
    ret
} // end of method Unsafe::Add

ジェネリック型のrefを要素数分加算する

こちらはnative int版

public static ref T Add<T>(ref T source, IntPtr elementOffset)
{
    return ref Unsafe.Add(ref source, elementOffset);
}
.method public hidebysig static !!T& Add<T>(!!T& source, native int elementOffset) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    sizeof !!T
    mul
    add
    ret
} // end of method Unsafe::Add

voidポインターをジェネリック型として要素数分加算する

public unsafe static void* Add<T>(void* source, int elementOffset)
{
    return (byte*)source + (long)elementOffset * (long)sizeof(T);
}
.method public hidebysig static void* Add<T>(void* source, int32 elementOffset) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    sizeof !!T
    conv.i
    mul
    add
    ret
} // end of method Unsafe::Add

ジェネリック型のrefへ指定バイト数分加算する

public static ref T AddByteOffset<T>(ref T source, IntPtr byteOffset)
{
    return ref Unsafe.AddByteOffset(ref source, byteOffset);
}
.method public hidebysig static !!T& AddByteOffset<T>(!!T& source, native int byteOffset) cil managed aggressiveinlining
{
    .maxstack 2
    ldarg.0
    ldarg.1
    add
    ret
} // end of method Unsafe::AddByteOffset

ジェネリック型のrefを要素数分減算する

ref *(((T*)&source) - elementOffset)

public static ref T Subtract<T>(ref T source, int elementOffset)
{
    return ref Unsafe.Subtract(ref source, elementOffset);
}
.method public hidebysig static !!T& Subtract<T>(!!T& source, int32 elementOffset) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    sizeof !!T
    conv.i
    mul
    sub
    ret
} // end of method Unsafe::Subtract

ジェネリック型のrefを要素数分加算する

こちらはnative int版

public static ref T Subtract<T>(ref T source, IntPtr elementOffset)
{
    return ref Unsafe.Subtract(ref source, elementOffset);
}
.method public hidebysig static !!T& Subtract<T>(!!T& source, native int elementOffset) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    sizeof !!T
    mul
    sub
    ret
} // end of method Unsafe::Subtract

voidポインターをジェネリック型として要素数分減算する

ref *(((T*)source) - elementOffset)

public unsafe static void* Subtract<T>(void* source, int elementOffset)
{
    return (byte*)source - (long)elementOffset * (long)sizeof(T);
}
.method public hidebysig static void* Subtract<T>(void* source, int32 elementOffset) cil managed aggressiveinlining
{
    .maxstack 3
    ldarg.0
    ldarg.1
    sizeof !!T
    conv.i
    mul
    sub
    ret
} // end of method Unsafe::Subtract

ジェネリック型のrefへ指定バイト数分減算する

ref *((T*)(((byte*)&source) - byteOffset))

public static ref T SubtractByteOffset<T>(ref T source, IntPtr byteOffset)
{
    return ref Unsafe.SubtractByteOffset(ref source, byteOffset);
}
.method public hidebysig static !!T& SubtractByteOffset<T>(!!T& source, native int byteOffset) cil managed aggressiveinlining
{
    .maxstack 2
    ldarg.0
    ldarg.1
    sub
    ret
} // end of method Unsafe::SubtractByteOffset

refと同じ型の別のrefの差を求める

(byte*)(ref right) - (byte*)(ref left)

public static IntPtr ByteOffset<T>(ref T origin, ref T target)
{
    return Unsafe.ByteOffset(ref target, ref origin);
}
.method public hidebysig static native int ByteOffset<T>(!!T& origin, !!T& target) cil managed aggressiveinlining
{
    .maxstack 2
    ldarg.1
    ldarg.0
    sub
    ret
} // end of method Unsafe::ByteOffset

アドレス比較系

refが同じ値かどうかを比較する

public static bool AreSame<T>(ref T left, ref T right)
{
    return (ref left) == (ref right);
}
.method public hidebysig static bool AreSame<T>(!!T& left, !!T& right) cil managed aggressiveinlining
{
    .maxstack 2
    ldarg.0
    ldarg.1
    ceq
    ret 
} // end of method Unsafe::AreSame

refが右辺より大きいか比較する

public static bool IsAddressGreaterThan<T>(ref T left, ref T right)
{
    return (ref left) > (ref right);
}
.method public hidebysig static bool IsAddressGreaterThan<T>(!!T& left, !!T& right) cil managed aggressiveinlining
{
    .maxstack 2
    ldarg.0
    ldarg.1
    cgt.un
    ret 
} // end of method Unsafe::IsAddressGreaterThan

refが右辺より小さいか比較する

public static bool IsAddressLessThan<T>(ref T left, ref T right)
{
    return (ref left) < (ref right);
}
.method public hidebysig static bool IsAddressLessThan<T>(!!T& left, !!T& right) cil managed aggressiveinlining
{
    .maxstack 2
    ldarg.0
    ldarg.1
    clt.un
    ret 
} // end of method Unsafe::IsAddressLessThan

マーシャリングを考慮しないサイズを求める

unsafeコンテキストでsizeof()することと同じ。
Marshal.SizeOf()はマーシャリングを考慮する。

個人的にはwhere T : unmanagedをつけて欲しい。

public unsafe static int SizeOf<T>()
{
    return sizeof(T);
}
.method public hidebysig static int32 SizeOf<T>() cil managed aggressiveinlining
{
    .maxstack 1
    sizeof !!T
    ret
} // end of method Unsafe::SizeOf

.NET Core 3以降またはNuGet パッケージの最新(少なくとも4.7.1以降)

box化された構造体(オブジェクト参照)の内部のrefを取得する

C++/CLIで云うcli::interior_ptrと等価。

public static ref T Unbox<T>(object box) where T : struct
{
    return ref (T)box;
}
.method public hidebysig static !!T& Unbox<valuetype .ctor ([CORE_ASSEMBLY]System.ValueType) T> (object 'box') cil managed aggressiveinlining
{
    .maxstack 1
    ldarg.0
    unbox !!T
    ret
} // end of method Unsafe::Unbox

.NET 5以降

初期化(0クリア)をスキップして変数宣言する

多分CLRの対応が必要 https://github.com/dotnet/runtime/issues/29905

  .method public hidebysig static void SkipInit<T> ([out] !!T& 'value') cil managed aggressiveinlining
  {
        .maxstack 0
        ret
  } // end of method Unsafe::SkipInit

個人的に欲しいメソッド

組み合わせれば実現するが、できれば直接欲しいと思っているメソッドの一覧。

  • Cで云うoffsetof
  • Cで云うmemmove
    cpblkに領域が重なっている場合の保証がないので。
  • readonly refへの強制再解釈
  • 全体的にreadonly ref版のメソッド 必要無ければ、readonlyにしてほしいところだが、オーバーロードできないので多分厳しい。
  • アライメント判定
  • Overlaps判定(Span<T>にはあるけど。)
  • ジェネリック型T where T : unmanagedのマネージドポインターをポインターへの再解釈 public unsafe static T* ATypedsPointer<T>(ref T value) where T : unmanaged { return (T*)Unsafe.AsPointer(ref value); }
6
3
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
6
3