ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

(C#) 参照型インスタンスのアドレスを取得する

C# では unsafe キーワードを使うことで値型のポインタや、unmanaged型配列の配列要素へのポインタを取得できますが、 参照型インスタンスへのポインタは取得できません。ガベージコレクション (GC) によるコンパクションでアドレスが移動する可能性があり、C# 側からはメモリ移動を検知する手段も用意されていないため言語仕様として当然です。

が、System.Runtime.CompilerServices.Unsafeクラスを使うと実は取得できます。 Unsafeクラスについては以前の記事にも少し書きましたが、かなり乱暴なことができます。unsafeキーワード以上にアンセーフです。

この記事の内容はたぶん言語としての動作保証仕様外。言語仕様の外に片足突っ込んてるそれなりにハックな内容。

手法

// 何かしらの参照型
class Sample()
{
}
unsafe struct Pointer
{
    public void* P;
}

// ~~~~~~~~~~~~~~~~~~~~~~~~

// 参照型インスタンスのポインタを取得
var sample = new Sample();
ref byte refSample
        = ref Unsafe.AsRef<byte>(Unsafe.As<Sample, Pointer>(ref sample).P);

Sampleはクラスです。クラスへのポインタは言語仕様としては取れませんが、強引に取得します。

まず、上記のようにvoid*型のフィールドを1つだけ持つ構造体を用意し、 Unsafe.As()をつかって参照型インスタンスをこの構造体に強制キャストします。この時、得られるvoid*型のフィールドが参照型インスタンスのアドレスです。

void* address = Unsafe.As<Sample, Pointer>(ref sample);

[2020/07/11 追記] よく考えると上記の「void*型のフィールドを1つだけもつ構造体」はIntPtrそのものなので、 わざわざ自分で用意するまでもなくIntPtrでできますね。

これだけで目的は達しているのですが、前述の通りGCのコンパクションで移動しうるので、 このポインタはいつどのタイミングで無効なポインタになってもおかしくないです。 そこで、このポインタをref byteに変換します。

ref byte refSample = ref Unsafe.AsRef<byte>(address);

refにするとGCによるトラッキングを受けられるため、コンパクションによるメモリ移動時に無効な参照になりません。 本来、参照型に対してrefなんて使えるものではないのに本当にGCにトラッキングされるのか?という疑問が当然出ますが、実際確認した結果、きちんとトラッキングされていました。 確認したコードは以下の gist に。

gist.github.com

安全性

本当にコンパクションによるメモリ移動で無効な参照を引かないのか。 正直なところ100%安全だと私は断言できませんでした。でもまあ十中八九大丈夫でしょう。

JITコンパイル結果を見ます。.NET Core で JITは64bitのリリースビルドを見ます。 JIT結果は sharplab で確認しました。リンクはこちら

参照型としてstring型を見てますが、別に参照型なら何でも同じです。参照型インスタンスref byteに変換するメソッドを用意しました。

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe ref byte GetRef(string obj)
{
    return ref Unsafe.AsRef<byte>(Unsafe.As<string, Pointer>(ref obj).P);
}

このメソッドの JIT コンパイル結果 (amd64) は以下の通り。

C.GetRef(System.String)
    L0000: mov [rsp+0x10], rdx
    L0005: mov rax, [rsp+0x10]
    L000a: ret

スタックに乗せて、乗せた値を返しているだけです。 当然と言えば当然ですが、引数を素通しで返すだけの虚無みたいな逆コンパイル結果が吐かれます。テンションが上がりますね。

Unsafe.AsRef(), Unsafe.As() のどちらも.NET Core のソースコード (AsRef(), As() ) を見てもらえば分かりますが (どちらも C# で書けないため IL で書かれてますが)、両者とも引数を素通しするだけの非常にクールでイカしたメソッドなので、 最適化によって何もしない虚無が生成されます。非常にテンション上がりますね。結果、上記のようになるわけですけど、これって GC が挟まる余地があるんでしょうか?

ref byteになってしまった後のコードは全て GC のトラッキングを受けられるため安全ですが、ILの型として一瞬ポインタを経由する瞬間に移動したら死亡しますが、その部分って最適化で跡形もなく消えてるのでは?と思うため個人的には安全だと思っています。

しかし、私自身が C#コンパイラ .NET のランタイムのソースコードを読んだりしていないため、何とも言えません。 あくまで言語仕様外のことを強引に行っているのをお忘れなく。。。

その他

コンパクションで移動しうる参照をrefでつかむとコンパクションに対して確実に安全なのだとしたら、マネージド配列に対するfixedっていらなくない?

同じようなことやっている記事を見かけたのだけれど。

azyobuzin.hatenablog.com

なんというか、MSからUnsafeクラスなんていう危険なオモチャが公式で提供されてるのですから、遊ばずにはいられないですよね。。。