👻

Unity 2022.3でref fieldする(ついでにいろいろなUnsafe)

2023/11/24に公開

Unity 2022.3でref fieldしたいですよね?

2023/11/24現在、CoreCLR対応が進んでたりして、最新の.Netを使ってref fieldする日も遠くないと思いますが、今すぐref fieldしたいと思い立ったので、実現させます。
結論から言うと、Unity 2022.3、Windows環境でEditor, Mono build, IL2CPP buildでともにref fieldが出来ました。試していませんが、Span<T>のあるversionなら2022.3以前でも動くと思います。

黒魔術なしで実現する方法

すでに記事にされた方がいるのでこちらの記事を見てください。
Span<T>が持つref T と同様のByReference<T>をを介してrefをやり取りするというもの。
欠点はいらないint Lengthが入ってしまっている点です。
ByReference<T>はmscorlib.dll内のinternalなので、直接我々は使えないのです。
しかも特別扱いされているので、ILで書くことすらできないのです。
、、じゃあmscorlib.dllを書き換えれば、、

黒魔術で実現する方法

mscorlib.dllをいじって、ByReference<T>のみをフィールドに持つpublic ref structを差し込んでしまいましょう。
どうやって?黒魔術 Mono.Cecilです。
Unityではmanifest.jsonのdependenciesに追加することでつかえます。

{
  "dependencies": {
    "com.unity.nuget.mono-cecil": "1.11.4"
    ...

Editorコンパイル用には
\Unity\Hub\Editor[Editor version]\Editor\Data\UnityReferenceAssemblies[API version]\mscorlib.dll
Windows Editor/MonoBuild Runtimeでは
\Unity\Hub\Editor[Editor version]\Editor\Data\MonoBleedingEdge\lib\mono\unityjit-win32\mscorlib.dll
Windows IL2CPPBuild Runtimeでは
\Unity\Hub\Editor[Editor version]\Editor\Data\MonoBleedingEdge\lib\mono\unityaot-win32\mscorlib.dll
をそれぞれ書き換えましょう。
(バックアップはちゃんと取ろうね!!)
黒魔術のコードはこちら(https://gist.github.com/Akeit0/3dff421b719acd9cdb8ac6121404e30a)に張っておくとして差し込んだref struct (名前はByRef<T>)はDecompileするとこんな感じです。

using System.Runtime.CompilerServices;
namespace System
{
  public readonly ref struct ByRef<T>
  {
    internal readonly ByReference<T> _pointer;
    
    public ref T Value
    {
      [MethodImpl(MethodImplOptions.AggressiveInlining)] get => ref this._pointer.Value;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public ByRef(ref T ptr) => this._pointer = new ByReference<T>(ref ptr);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public unsafe ByRef(void* ptr) => ^(IntPtr&) ref this = (IntPtr) ptr;
  }
}
public unsafe ByRef(void* ptr) => ^(IntPtr&) ref this = (IntPtr) ptr;

ってなんだって感じなのでちょっと分かりやすく書くとこんな感じ

public unsafe ByRef(void* ptr) =>  *(void**)&this = ptr;

ILでは

.method public hidebysig specialname rtspecialname instance void
    .ctor(
      void* ptr
    ) cil managed
  {
    .maxstack 8
    IL_0000: ldarg.0      // this
    IL_0001: ldarg.1      // ptr
    IL_0002: stind.i
    IL_0003: ret

  } // end of method ByRef`1::.ctor

ようは自身への参照をref voidとして voidを代入するってことです。

まあほかはByReference<T>をref Tで作ったり、ref T を取り出したりしているだけです。

これでUnityでref fieldが使えるようになりましたね。

ちなみにこれはref fieldが準備されてないことによる制限のゆるさなのですが、StructLayout ExplicitでByReference<T>とほかのunmanaged型を重ねられます。

[StructLayout(LayoutKind.Explicit)]
public ref struct Union {
    [FieldOffset(0)]
    public ByRef<byte> Ref;
    [FieldOffset(0)]
    public IntPtr Ptr;
}

どうせならILでしかできないことをしよう

ref fieldとUnsafe classがあればたいていのUnsafeなことが可能ですが、できないことがあります。
それはref to ref structのポインタ化およびポインタからのref to ref structです。
Unsafe.AsPointer<T>(ref T ptr)を使いたいですが、ref structはGeneric引数にできないので、それぞれILで書かないといけないのです。
ということで、実は先ほどの黒魔術で、それぞれの関数を追加しています。

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe void* AsPointer(ref ByRef<T> value) => (void*) ref value;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static unsafe ref ByRef<T> AsRef(void* value) => ref (*(ByRef<T>*) value);

ref はスタックに乗っていることが保証されているので、ポインタとの相互変換自体は問題ないはずです。

これにより、ref arrayが簡単に書けます。

[StructLayout(LayoutKind.Sequential)]
public unsafe ref struct FixedByRefArray4<T> {
    public ByRef<T> A;
    public ByRef<T> B;
    public ByRef<T> C;
    public ByRef<T> D;
    public ref ByRef<T> this[int index] => ref ByRef<T>.AsRef((IntPtr*) ByRef<T>.AsPointer(ref A) + index);
}

まとめ

黒魔術でref fieldのないUnity環境にref fieldを導入できるようにしました。
ついでにarray of refを使えるようになりました。
できればILは書きたくありませんが、こういったこともできるのがILの強みですね。
黒魔術(https://gist.github.com/Akeit0/3dff421b719acd9cdb8ac6121404e30a)

おまけ C#のスタック上ではrefとobjectは同じ?

あとで書きます。

Discussion