[C#] UnsafeAccessor 属性を使ったコードが実行時に BadImageFormatException 例外を吐いた
UnsafeAccessor
属性とは
2023年11月の .NET 8 の登場により、C# のバージョン 12 によって、リフレクションを使う以外の方法で非パブリックなメンバーにアクセスする方法として、UnsafeAccessor
属性というのが使えるようになりました。
公式ドキュメントは下記リンクで、実際の使用例も豊富に掲載されています。
実際の利用例
例えば以下のような非パブリックなフィールド _elementId
を持つクラス FooComponent
があったとして、
public class FooComponent {
private string? _elementId;
...
}
上記 FooComponent
のインスタンスを持つ別のクラス BarComponent
があり、その中で前述の非パブリックメンバ _elementId
の値を取得したいとします。
public class BarComponent {
private FooComponent _foo;
private void DoSomething() {
// ここで、_foo._elementId の値を取得したい!
// (しかし _elementId は FooComponent の private フィールドなので触れない!)
}
}
ここで、UnsafeAccessor
属性を使って、FooComponent
クラスの _elementId
フィールドへアクセスするメソッドを実装できます。
public class BarComponent {
private FooComponent _foo;
// 以下のように FooComponent の _elementId にアクセスするメソッドを記述
// (実装はコンパイラによって提供されるので extern 指定)
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_elementId")]
private static extern ref string? GetElementId(FooComponent foo);
private void DoSomething() {
// すると、上記メソッドを使って、以下のように _elementId を取得できる!
var elementId = GetElementId(_foo);
...
}
}
上記例では BarComponent
の static メンバーとして UnsafeAccessor
メソッドを実装しましたが、メソッド内の static ローカル関数としても実装できます。なので、本当に1箇所でしかアクセスしないというときは、ローカル関数のほうがスコープが限定されてよりよいと思います。
UnsafeAccessor
登場の背景
ところで、当然のことながら非パブリックなメンバーへのアクセスは決して推奨されるものではありません。そうはいっても、一時的なモンキーパッチが求められるときであったり、取り急ぎ単体テストを機能させたいときなど、必要に迫られることもあります。そのような場面では、.NET 8 の登場より前は、リフレクションの機能を使って非パブリックメンバーにアクセスするプログラムを書いていました。しかしリフレクションを使ったプログラミングは、コードが冗長になりがちで可読性に欠けますし、処理速度が遅いといった問題を抱えていました。それが、.NET 8 以後は、これまではリフレクションを使う他なかった多くの場面で、UseAccessor
属性による、簡潔でより高速な非パブリックメンバーへのアクセスが実装できるようになりました。
ある日 BadImageFormatException 例外に遭遇
さてそのように、差し迫った場面で便利な UnsafeAccesor
属性による非パブリックメンバーアクセスなのですが、ある日、先の「利用例」で書いたようなコードを新たに実装し、ビルド (もちろんビルドは成功)、そしていよいよ実行したところ、以下のような例外を吐いてクラッシュしてしまいました。
Throws System.BadImageFormatException occurred in *.dll:
Invalid usage of UnsafeAccessorAttribute.
なんどもコードを読み返すもタイポもなく、アクセス対象のメンバーのシグネチャもどう見ても一致しています。
原因は Generics を使っていたから、らしい
思案に暮れながら某生成系 AI チャットボットに検索させたところ、「Generics クラス内で UnsafeAccessor 使うと発生することがあるらしい」と教えてくれました。そうなんです、実は今回 BadImageFormatException 例外が発生したコードは、前述の「実際の利用例」の箇所に記述したコードとほぼほぼ同じなのですが、一点、UnsafeAccessor
を利用しているクラスがジェネリクス型なのでした。
// Generics 使っていた! 👇
public class BarComponent<T> {
private FooComponent _foo;
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_elementId")]
private static extern ref string? GetElementId(FooComponent foo);
private void DoSomething() {
var elementId = GetElementId(_foo);
...
}
}
ということで、やむなく UnsafeAccessor
属性による非パブリックメンバーにアクセスするメソッドを、別の非ジェネリックなクラスに移設したところ、無事、正常に動作するようになりました。
// 非ジェネリックなクラスに移設。せっかくなので拡張メソッドの体裁にした。
public static class FooComponentAccessor {
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_elementId")]
public static extern ref string? GetElementId(this FooComponent foo);
}
public class BarComponent<T> {
private FooComponent _foo;
private void DoSomething() {
// これで OK!
var elementId = _foo.GetElementId();
...
}
}
.NET 9 では問題解消されている
ちなみに、2024年11月にリリースされた .NET 9 / C# 13 では、この問題は解消されているようです。なので、ジェネリッククラス内で UnsafeAccessor
属性を使ったコードを実装しても問題なく、期待どおり動作します。
Discussion