【C#】固定長配列をリフレクションで操作する
固定長配列自体がかなりマイナーな言語機能ですが、今回はさらにそれをリフレクション経由で操作するという記事です。
固定長配列の内部
まず、普通の固定長配列の使い方は次のような感じです。
var s = new S();
unsafe
{
s._buffer[0] = 255;
}
public struct S
{
public unsafe fixed byte _buffer[12];
}
固定長配列_bufferをもつ構造体Sは、コンパイラによって次のようなコードに展開されます。
public struct S
{
[StructLayout(LayoutKind.Sequential, Size = 12)]
[CompilerGenerated]
[UnsafeValueType]
public struct <_buffer>e__FixedBuffer
{
public byte FixedElementField;
}
[FixedBuffer(typeof(byte), 12)]
public <_buffer>e__FixedBuffer _buffer;
}
なるほど、StructLayoutでサイズが12に固定された構造体が自動的に生成されており、その構造体が_bufferの型になっているようです。
リフレクションで取得してみる
まずは普通にリフレクションでフィールド_bufferを取得してみます。
using System.Reflection;
var s = new S();
unsafe
{
s._buffer[0] = 255;
}
object sBoxed = s;
var bufferField = typeof(S).GetField("_buffer");
var buffer = bufferField.GetValue(sBoxed);
普通に取得できましたが、ここからどうするかがちょっと問題です。
取得できたbufferはランタイムでは<_buffer>e__FixedBuffer型ですが、コード上はobjectになっており、ボックス化されています。
例えば、ここからさらにbuffer.GetType().GetField("FixedElementField")を呼んでみたとしても、先頭の要素にしかアクセスできません。
では、ボックス化を解除してみるのはどうでしょう?
var bufferUnboxed = (S.<_buffer>e__FixedBuffer)s;
残念ながら、これはコンパイルが通りません。コンパイラが自動的に生成する型<_buffer>e__FixedBufferにはC#コードからアクセスできませんし、そもそもC#では型名として使用できない文字が入っているので、構文として不正です。
Type.MakeGenericType()を使う
コンパイル時に不明な型をランタイムで使うといえばジェネリクスの出番です。わからない型はTにしておいて、実行時にバインドしてしまえばいいのです。
using System;
// ...略...
var wrapperType = typeof(FixedArrayWrapper<>).MakeGenericType(buffer.GetType());
var wrapper = Activator.CreateInstance(wrapperType, buffer);
struct FixedArrayWrapper<T> where T : unmanaged
{
private T unboxed;
public FixedArrayWrapper(object boxed)
{
unboxed = (T)boxed;
}
}
ところで、このように型のバインドを遅延できるのは実行時にコードを生成できるJITならではで、一部のAOT(事前コード生成)環境では動作しないようです。
例えば.NET本家のAOTターゲットであるNativeAOTではMakeGenericType()が正しく動作しない場合があるようです。
UnityのIL2CPPでも同様の制約があったようですが、バージョン2022.1以降ではFull Generic Sharingという技術により対応可能になったという言及があります。
ということで、実行環境が限られるという条件付きではありますが、ボックス化の解除に成功しました。
要素を取得する
次はボックス化が解除された<_buffer>e__FixedBufferから要素を取得してみましょう。
これには<_buffer>e__FixedBufferをbyteかのように扱うことが必要です。
こういう操作はSpan<T>やMemoryMarshalの得意分野です。
using System.Runtime.InteropServices;
class FixedArrayWrapper<T> where T : unmanaged
{
private T unboxed;
public FixedArrayWrapper(object boxed)
{
unboxed = (T)boxed;
}
public ref byte this[int index] => ref MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref unboxed, 1))[index];
}
さらに、動的に生成したFixedArrayWrapper<T>はobjectのままではアクセスできないので、新たにIFixedArrayWrapperインターフェースを作成してダウンキャストします。
これでボックス化を解除したbufferの内容にアクセスできるようになりました。
using System.Runtime.InteropServices;
// ...略...
var wrapperType = typeof(FixedArrayWrapper<>).MakeGenericType(buffer.GetType());
// ダウンキャスト
var wrapper = Activator.CreateInstance(wrapperType, buffer) as IFixedArrayWrapper;
var a = wrapper[0];
wrapper[1] = 255;
interface IFixedArrayWrapper
{
ref byte this[int index] { get; }
}
class FixedArrayWrapper<T> : IFixedArrayWrapper where T : unmanaged
{
private T unboxed;
public FixedArrayWrapper(object boxed)
{
unboxed = (T)boxed;
}
public ref byte this[int index] => ref MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref unboxed, 1))[index];
}
変更を適用する
最後に、変更を加えた<_buffer>e__FixedBufferをもとのフィールドに代入しなおします。これにはボックス化を解除したunboxedをふたたびボックス化して、あとは単にSetFieldしてあげればOKです。
全体像はこんな感じになりました。
using System;
using System.Reflection;
using System.Runtime.InteropServices;
var s = new S();
unsafe
{
s._buffer[0] = 255;
}
object sBoxed = s;
var bufferField = typeof(S).GetField("_buffer");
var buffer = bufferField.GetValue(sBoxed);
var wrapperType = typeof(FixedArrayWrapper<>).MakeGenericType(buffer.GetType());
// ダウンキャスト
var wrapper = Activator.CreateInstance(wrapperType, buffer) as IFixedArrayWrapper;
var a = wrapper[0];
wrapper[1] = 255;
var modifiedBuffer = wrapper.Box();
bufferField.SetValue(sBoxed, modifiedBuffer);
// s自体をボックス化解除
s = (S)sBoxed;
public struct S
{
public unsafe fixed byte _buffer[12];
}
interface IFixedArrayWrapper
{
object Box();
ref byte this[int index] { get; }
}
class FixedArrayWrapper<T> : IFixedArrayWrapper where T : unmanaged
{
private T unboxed;
public FixedArrayWrapper(object boxed)
{
unboxed = (T)boxed;
}
public object Box() => unboxed;
public ref byte this[int index] => ref MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref unboxed, 1))[index];
}
要素型も動的に決定する
ここまでの内容では、固定長配列の要素型がbyteである前提で話をしていましたが、ここもやはり動的に決定したいところですので、詳しい説明は省略しますがコードだけ載せておきます。とはいえ、それほど複雑なことはありません。
using System;
using System.Runtime.InteropServices;
var s = new S();
unsafe
{
s._buffer[0] = 255;
}
object sBoxed = s;
var bufferField = typeof(S).GetField("_buffer");
var buffer = bufferField.GetValue(sBoxed);
// FixedElementFieldフィールドの型で要素型がわかる
var elementType = buffer.GetType().GetField("FixedElementField").FieldType;
var wrapperType = typeof(FixedArrayWrapper<,>).MakeGenericType(buffer.GetType(), elementType);
// ダウンキャスト
var wrapper = Activator.CreateInstance(wrapperType, buffer) as IFixedArrayWrapper;
var a = wrapper[0];
wrapper[1] = (byte)255;
var modifiedBuffer = wrapper.Box();
bufferField.SetValue(sBoxed, modifiedBuffer);
// s自体をボックス化解除
s = (S)sBoxed;
public struct S
{
public unsafe fixed byte _buffer[12];
}
interface IFixedArrayWrapper
{
object Box();
object this[int index] { get; set; }
}
class FixedArrayWrapper<T, TElement> : IFixedArrayWrapper where T : unmanaged where TElement : unmanaged
{
private T unboxed;
public FixedArrayWrapper(object boxed)
{
unboxed = (T)boxed;
}
public object Box() => unboxed;
private ref TElement GetItem(int index) => ref MemoryMarshal.Cast<T, TElement>(MemoryMarshal.CreateSpan(ref unboxed, 1))[index];
public object this[int index]
{
get => GetItem(index);
set => GetItem(index) = (TElement)value;
}
}
ということで以上です。
書いておいてなんですが、ニッチすぎて一体何人の役に立つことやら……
Discussion