💣

[C#] UnsafeAccessor 属性を使ったコードが実行時に BadImageFormatException 例外を吐いた

2025/03/13に公開

UnsafeAccessor 属性とは

2023年11月の .NET 8 の登場により、C# のバージョン 12 によって、リフレクションを使う以外の方法で非パブリックなメンバーにアクセスする方法として、UnsafeAccessor 属性というのが使えるようになりました。

公式ドキュメントは下記リンクで、実際の使用例も豊富に掲載されています。

https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.unsafeaccessorattribute

実際の利用例

例えば以下のような非パブリックなフィールド _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