Start Debugging

Fix: PlatformNotSupportedException: Operation is not supported on this platform (Native AOT)

Native AOT は JIT とインタープリターを取り除くため、reflection emit、式ツリーのコンパイル、未知の MakeGenericType は実行時に例外を投げます。IL3050 で呼び出しを特定し、ソースジェネレーターまたは事前生成された経路に置き換えます。

修正方法: Native AOT は JIT もインタープリターも持たない単一のネイティブバイナリを発行するため、実行時に IL を生成するコード経路、Expression<T> ツリーをコンパイルする経路、ランタイムにこれまで見たことのないジェネリックインスタンス化を作らせる経路はいずれも PlatformNotSupportedException を投げます。最初の一手は常に同じです。dotnet publish -r <rid> -c Release で再ビルドし、原因のメンバーを名指しする IL3050 警告を読み、AOT 互換の代替(ソースジェネレーター、事前インスタンス化されたジェネリック、または機能スイッチ)に置き換えます。

System.PlatformNotSupportedException: Operation is not supported on this platform.
   at System.Reflection.Emit.DynamicMethod..ctor(String name, Type returnType, Type[] parameterTypes)
   at SomeLibrary.Internal.ExpressionCompiler.Compile(LambdaExpression expr)
   at SomeLibrary.PublicEntryPoint.DoTheThing()
   at Program.<Main>$(String[] args) in /src/Program.cs:line 12
System.PlatformNotSupportedException: Dynamic code generation is not supported on this platform.
   at System.Reflection.Emit.AssemblyBuilder.DefineDynamicAssembly(...)
   at System.Linq.Expressions.Compiler.LambdaCompiler.CompileLambda(LambdaExpression lambda, Boolean hasClosureArgument)
   at System.Linq.Expressions.Expression`1[[T]].Compile()

このガイドは .NET 11 SDK (preview 4)、Microsoft.NET.Sdk 11.0.0-preview.4、C# 14 を対象としています。例外の文言と背後にある制約は、Native AOT が .NET 8 でサポート対象のデプロイメントとして出荷されてから安定しているため、以下の内容は .NET 8、10、11 で変更なくそのまま当てはまります。.NET 9 では IL3050 を支える RequiresDynamicCodeAttribute のアナライザー周りが整備され、.NET 11 ではさらに多くの BCL の経路が AOT セーフな実装に置き換えられて締め付けが強化されています。

異なる 2 つのメッセージが同じ例外型を共有しています。Operation is not supported on this platform は JIT-emit のファストパスが RuntimeFeature.IsDynamicCodeSupported == false に当たって出るものです。Dynamic code generation is not supported on this platform は何かが DynamicAssembly または DynamicMethod を構築しようとしたときに Reflection.Emit 自身から出るものです。どちらも根本原因と修正の打ち手は同じなので、別々のバグとして扱って時間を浪費しないでください。

単一ファイルの Native AOT バイナリが IL を生成できない理由

dotnet publish /p:PublishAot=true は完全に事前コンパイルされたネイティブイメージを生成します。JIT は静的にリンクされていません。Mono インタープリターも静的にリンクされていません。CoreCLR の Reflection.Emit ライターはコンパイル時に取り除かれます。実行時に System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupportedfalse を返し、新しい IL の書き出しや、生のマシンコードからの新しいデリゲートの組み立てに依存するあらゆる API は、IsSupported 形式のガード失敗かこの例外のいずれかを返します。

実コードでこの制約に当たるパターンは 4 つです:

  1. Reflection.Emit の直接利用。 DynamicMethodAssemblyBuilder.DefineDynamicAssemblyTypeBuilderILGenerator.Emit。これらは即座に例外を投げます。
  2. 非自明なツリーに対する Expression<T>.Compile() 式コンパイラーは内部的に DynamicMethod まで降ろします。Compile(preferInterpretation: true) は .NET 8 と 10 ではインタープリターにフォールバックしますが、インタープリターも Native AOT からは取り除かれているため、BCL 側にツリーをたどるフォールバックがない限り true のオーバーロードでも例外が出ます。
  3. コンパイラーが見ていない型引数を伴う Type.MakeGenericType / MethodInfo.MakeGenericMethod List<int> は AOT コンパイラーがインスタンス化したので動きます。コンパイラーが到達していない値型に対する MakeGenericType(someTypeFromReflection) は例外を投げます。
  4. 以上に積み重なるあらゆるもの。 AutoMapper の式コンパイル済みマッパー、FastMember、MediatR のオープンジェネリック、Newtonsoft.Json のコンバーターキャッシュ、model builder がカバーしていない POCO グラフ上の EF Core のコンパイル済みクエリ経路、古い codegen を使う gRPC.Core、そして動的プロキシに依存する Dataverse / Service Fabric / WCF のクライアント。

JIT ビルドではランタイムが新しいメソッドの tier-0 コードを生成して先に進みますが、Native AOT には新しいコードを置く場所がないため、API が呼び出された最初の瞬間に例外を投げます。

最小再現

// .NET 11 SDK preview 4, C# 14, <PublishAot>true</PublishAot>
using System.Linq.Expressions;

Expression<Func<int, int>> expr = x => x + 1;
var compiled = expr.Compile();   // throws on Native AOT
Console.WriteLine(compiled(41));

.csproj:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net11.0</TargetFramework>
    <PublishAot>true</PublishAot>
    <InvariantGlobalization>true</InvariantGlobalization>
  </PropertyGroup>
</Project>

Linux x64 で発行します:

dotnet publish -r linux-x64 -c Release

publish ステップは次のように出力します:

warning IL3050: Using member 'System.Linq.Expressions.Expression`1<TDelegate>.Compile()' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. Compiling a lambda requires dynamic code.

バイナリを実行します:

Unhandled exception. System.PlatformNotSupportedException: Dynamic code generation is not supported on this platform.

これがエラーの典型的な姿です。原因が自分のコードであっても、NuGet パッケージであっても、推移的に取り込まれたフレームワークの一部であっても、形は同じです。

修正方法、詳細

以下の修正は、コストの低いものから侵襲性の高いものへと並べてあります。最初にコンパイルが通るものを採用してください。

1. AOT 互換の代替に置き換える(推奨)

問題のある API のほぼすべてに、まさにこの例外クラスのために .NET チームが用意した AOT フレンドリーな置き換えがあります。

AOT に敵対的AOT フレンドリーな置き換え
Newtonsoft.Json[JsonSerializable] ソースジェネレーターのコンテキストを持つ System.Text.Json
AutoMapper (式コンパイル方式)ソースジェネレートされたマッパー(Mapster の source-generator モード、Riok.Mapperly、MapTo
MediatR (オープンジェネリック登録)手書きのハンドラー インターフェース、または Mediator(ソースジェネレーター版)
Microsoft.AspNetCore.Mvc.ControllersWebApplication.CreateSlimBuilder + JsonSerializerContext を伴う minimal API
実行時に構築される EF Core モデルモデルを事前生成する OptimizeQuery / dotnet ef dbcontext optimize
Castle.DynamicProxy のインターセプターソースジェネレートされたデコレーター(Roslyn や PolySharp 経由など)
Expression<T>.Compile()delegate*<...>Func<...> リテラル、またはビルド時にコンパイルされる手書きの IL 置き換え

ラムダのコンパイル呼び出しを置き換える具体例:

// .NET 11, C# 14
// Before: AOT-hostile
Expression<Func<int, int>> expr = x => x + 1;
var compiled = expr.Compile();

// After: pure delegate, no expression tree, no Compile()
Func<int, int> compiled = x => x + 1;

ボディを検査するなど、本当に式ツリーの形が必要な場合はツリーは残しつつ、Compile() の呼び出しをやめて、消費側を自分で書いた delegate に切り替えます。

2. RuntimeFeature.IsDynamicCodeSupported を機能スイッチとして使う

動的経路がオプションであれば、ランタイムの機能フラグの背後にゲートし、遅いものの AOT セーフなフォールバックを出荷します:

// .NET 11, C# 14
using System.Runtime.CompilerServices;

public static T Materialize<T>(IDataReader reader)
{
    if (RuntimeFeature.IsDynamicCodeSupported)
    {
        return EmitMaterializer<T>.Compile()(reader);   // existing fast path
    }

    return ReflectionMaterializer<T>.Materialize(reader); // slower but no IL emit
}

AOT コンパイラーは発行されるバイナリに対して IsDynamicCodeSupportedfalse として静的に評価し、EmitMaterializer<T> を含む動的分岐をまるごとトリミングして取り除きます。IL3050 はもう出ませんし、実行時の例外もありません。重要: アナライザーがトリミングするのは、テストがプロパティの直接的な読み取りである場合に限ります。先にローカル変数に代入したり、メソッドにラップしたりすると、トリマーは死んだ分岐を残してしまいます。

3. コンパイラーから見えないジェネリックを事前にインスタンス化する

メッセージが MakeGenericType または MakeGenericMethod を名指ししている場合、AOT コンパイラーはどの T を生成すればよいかを知りません。出口は 2 つあります:

// .NET 11, C# 14
// Tell the AOT compiler exactly which generic instantiations to keep.
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
public class Repository<T> { /* ... */ }

// And in your DI bootstrap, use closed generics:
services.AddScoped<Repository<User>>();
services.AddScoped<Repository<Order>>();
// not: services.AddScoped(typeof(Repository<>));

純粋にリフレクションで呼んでいる場合は、ソースジェネレーター版の等価物に切り替えます。ASP.NET Core 11 は一般的な依存性注入の形について AOT セーフなオーバーロードを出荷していますし、ランタイム チームはパラメーターなしコンストラクター向けに AOT フレンドリーな Activator.CreateInstance<T>() のイントリンシックも追加しています。

4. メソッドに RequiresDynamicCode を付け、AOT 経路から呼び出さない

ライブラリを書いていてあるメソッドが本当に動的 codegen を必要とする場合、要件を呼び出し側へ伝播させて、アナライザーが呼び出し側のレベルで警告できるようにします:

// .NET 11, C# 14
[RequiresDynamicCode("Builds a per-type accessor with Reflection.Emit. " +
                     "Not supported in Native AOT. Use SourceGenAccessor<T> instead.")]
public static Func<object, object> BuildGetter(PropertyInfo prop) => /* emit */;

属性は実行時例外そのものを直しはしませんが、無言のクラッシュをビルド時の IL3050 へと変換します。これこそが Native AOT の利用者が期待している契約です。

5. 依存関係を取り除く

最後の手段。依存しているライブラリが Reflection.Emit をハードコードしていて AOT モードを提供していない場合、誠実な選択肢は (a) ライブラリを置き換える、(b) そのサブシステムを非 AOT のプロセス境界の向こう(HTTP や named pipe の境界の背後にある JIT 発行のワーカー)に置く、(c) 上流のメンテナーが AOT 経路を出荷するのを待つ、しかありません。例外を try/catch で握り潰してはいけません。失敗した操作にほぼ確実に依存している消費側がいるため、プログラムは未定義の状態になります。

よくある変種と紛らわしい例

本当に直ったかを確認する

勝利宣言の前に行う 3 つのチェック:

  1. dotnet publish -r <rid> -c ReleaseIL3050 警告を一切出さない。<TreatWarningsAsErrors>true</TreatWarningsAsErrors><IsAotCompatible>true</IsAotCompatible> を使ってエラーへ昇格させましょう。
  2. 発行されたバイナリが、以前失敗していた経路を DOTNET_TieredCompilation=0DOTNET_ReadyToRun=0 の下で実行する。Native AOT はこれらの環境変数を尊重しませんが、誤って self-contained JIT ビルドをテストしていないことを素早く確認するための手段になります。
  3. 発行されたバイナリのインポート テーブルに JIT (clrjit.dll / libclrjit.so) への参照が含まれていない。Linux ではバイナリに対して nm --dynamic をかけても clrjit シンボルは見えないはずです。

3 つすべてを通過すれば、AOT クリーンなバイナリができていて、同じ経路から例外が戻ってくることはありません。

関連

出典

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< 戻る