aot-trimming について
aot-trimmingは、ライブラリやアプリをトリミングとNative AOTに対応させる.NET開発者向けのリファレンススキルです。リンカー注釈や実行時リフレクションの代替策に加え、発行時の警告を確認する手順を解説します。
主な機能
- 01トリミングとNative AOTの動作を解説
- 02リンカー注釈の使い方を案内
- 03リフレクションに安全な実装パターンを紹介
- 04ソースジェネレーターと動的コード境界を説明
- 05発行と警告確認のチェックリストを提供
活用シーン
- 01.NETライブラリをトリミングに対応させる
- 02IL2xxxまたはIL3xxx警告を解決する
- 03コードベースのAOT互換性をレビューする
- 04実行時リフレクションをコンパイル時の手法に置き換える
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ aaronontheweb/ dotnet-skills --skill aot-trimmingインストール後、Claude Code で/aot-trimmingを入力して読み込まれたか確認します。
同梱ファイルaot-trimming-playbook-reference.mdtrimming-attributes-reference.md
詳細
.NET トリミングと Native AOT
使用する場面
- ライブラリやアプリケーションにトリミング(
PublishTrimmed)または Native AOT(PublishAot)のサポートを追加する IL2xxx(トリミング)およびIL3xxx(AOT/単一ファイル)の警告を解決する- リフレクションを多用するコードをトリミングセーフにする
- 実行時のリフレクション、アセンブリのスキャン、
Reflection.Emitをコンパイル時の代替手段に置き換える - 生成されたインターセプターやアナライザーの抑制も含め、トリミング/AOT 互換性についてコードベースや PR を確認する
- トリミング属性を付与する必要があるパブリック API を設計する
主な目標
- リンカーの到達可能性解析を通過し、Native AOT(JIT なし)で実行できるコードを作成します。
- コンパイラーから見えない情報、つまりリフレクションで必要となるメンバーを、トリミング属性で明示します。
- トリミングセーフなコードとトリミングセーフではないコードを分離し、セーフではない経路を明示して限定します。
基本モデル
トリミングと Native AOT の違い
- トリミング(
PublishTrimmed)では、ILLink による到達可能性解析が実行されます。静的に到達可能なコードだけが保持されます。アナライザーが検出できないリフレクション対象はトリミングされ、警告が発生します。 - Native AOT(
PublishAot)ではトリミングが必須になり、JIT が取り除かれます。Reflection.Emitはサポートされていません。実行時に 未知の ジェネリックの構築が保証されるわけではなく、Expression.Compile()はインタープリテーションにフォールバックする場合があります。また、リフレクションで使用できるのはリンカーが保持したメンバーのみです。 - どちらも同じ静的解析を実行します。
IL2xxx— トリミングの警告:RequiresUnreferencedCodeの伝播とDynamicallyAccessedMembersのデータフロー。IL3xxx— AOT および単一ファイルの警告:RequiresDynamicCode/RequiresAssemblyFiles。
ライブラリはトリミング可能でも、AOT 互換ではない場合があります(Reflection.Emit を使用しており、トリミングでは許容されても AOT では許容されません)。
解決すべき 2 つの問題
- 検出されないリフレクション(トリミング):リンカーが、名前で読み込むメンバーや、ジェネリックメソッド内で
typeof(T)を介して参照するメンバーまたは型を削除します。 - 動的コード生成(AOT):JIT がないため、
Reflection.Emitはサポートされず、未知の ジェネリックの構築も保証されません。
どちらも同じ方法で解決できます。必要なメンバーをアナライザーから 静的に見える ようにするか、リフレクションを完全に不要にします。
リフレクションは敵ではありません
リフレクションは正当な手法であり、通常の JIT アプリケーションではまったく問題ありません。そのような環境では、これらの対応は必要ありません。トリミング属性は、リフレクションを禁止するためではなく、トリミング/AOT 環境で使用できるようにするためにあります。リフレクションに必要なものに注釈を付ければ、リンカーが保持します。ソースジェネレーター、[UnsafeAccessor]、または機能の有無による制御を使うのは、特定のリフレクション対象の公開範囲をなくしたり制限したりしたい場合に限ります(たとえば、[RequiresUnreferencedCode] を付けて提供したくないパブリック登録 API など)。
プロジェクトの構成
ライブラリでは、アナライザーに警告を表示させ、利用者にも互換性が伝わるように宣言します。
<PropertyGroup>
<IsTrimmable>true</IsTrimmable>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
</PropertyGroup>
アプリケーションでは、トリミングまたは AOT を有効にして発行します。
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
<PublishAot>true</PublishAot>
</PropertyGroup>
実際に発行して確認してください。トリミング/AOT アプリでは警告が 0 件である必要があります。
dotnet publish -c Release -r <rid> -p:PublishTrimmed=true
dotnet publish -c Release -r <rid> -p:PublishAot=true
ライブラリの検証には、ライブラリをルートするテストアプリを使うのが標準的な方法です。
<PropertyGroup>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\MyLibrary\MyLibrary.csproj" />
<TrimmerRootAssembly Include="MyLibrary" />
</ItemGroup>
プロパティの一覧、機能スイッチ、警告承認ベースラインのパターンについては、aot-trimming-playbook-reference.md を参照してください。
属性モデル
これらは System.Diagnostics.CodeAnalysis にあります。正確なシグネチャ、警告コード、ルールを含む完全な一覧については、trimming-attributes-reference.md を参照してください。
[RequiresUnreferencedCode(message)]— リンカーが認識できないメンバーを必要とするコードをマークします。内部の警告を抑制し、すべての呼び出し箇所でIL2026を出力します。[RequiresDynamicCode(message)]— JIT(コードの生成や動的な処理)を必要とするコードをマークします。呼び出し箇所でIL3050を出力します。[DynamicallyAccessedMembers(memberTypes)]—Type/stringのどのメンバーを保持する必要があるかを宣言します。リフレクションの箇所からTypeのソースへと逆方向に伝播します。これとほかの属性を単一のアクセサーに適用するには、属性をアクセサーに直接付けるか、[method:]、[field:]、[return:]のいずれかのターゲットを指定します。[UnconditionalSuppressMessage(category, checkId, Justification = "...")]— 最終手段として使う、IL に永続化される抑制です。正当な理由と実際の不変条件を記述する必要があります。[DynamicDependency("Member", typeof(T))]— 名前を指定したメンバーを保持しますが、警告は抑制しません。
実際に行うリフレクションに対して、最も範囲の狭い DynamicallyAccessedMemberTypes を選択してください。
| リフレクション | メンバーの種類 |
|---|---|
Activator.CreateInstance<T>()、new T()、Activator.CreateInstance(type) | PublicParameterlessConstructor |
DI によるアクティベーション(ActivatorUtilities.CreateFactory<T>) | PublicConstructors |
| 非公開コンストラクターを明示的に使ったアクティベーション | NonPublicConstructors |
type.GetInterfaces() | Interfaces |
type.GetMethods() | PublicMethods(必要に応じて NonPublicMethods を追加) |
type.GetProperties() | PublicProperties |
type.GetFields() | PublicFields |
type.GetNestedTypes() | PublicNestedTypes |
パターンプレイブック(抜粋)
まず、コードをトリミング/AOT セーフにする必要があるかどうかを判断します。通常の JIT アプリやライブラリでは、これらは不要です。リフレクションをそのまま使えます。トリミングまたは AOT を有効にして発行するのが、あなた自身または利用者である場合に限り、リンカーが認識できる形でリフレクションを明示する必要があります。
目標は「すべてのリフレクションを削除する」ことではありません。リンカーが必要なものを保持できるよう、リフレクションを静的に認識可能にすることです。呼び出し箇所ごとに選択してください。
-
リフレクションを維持して注釈を付ける — 最も一般的なケースです。
Type/stringのソースに[DynamicallyAccessedMembers(...)]を付け、チェーン全体に伝播させます。エントリポイントには[RequiresUnreferencedCode]/[RequiresDynamicCode]を付けます。これらの属性は、トリミング環境でもリフレクションを機能させるために用意されています。trimming-attributes-reference.md を参照してください。 -
リフレクションをコンパイル時の代替手段に置き換える — リフレクションを使うインターフェイスをなくしたい場合(たとえば、
[RequiresUnreferencedCode]を付けずに公開したい登録 API)に選択します。ソースジェネレーターは直接的な型参照を生成し、マーカー属性から登録コードを生成できます。[UnsafeAccessor] はFieldInfo/MethodInfo/ConstructorInfoを置き換えます。aot-trimming-playbook-reference.md と aot-trimming-playbook-reference.md を参照してください。 -
動的なパス全体を分離する — プロキシの生成や名前によるプラグインの読み込みなど、本質的に動的なパスの場合は、境界の背後に移動し、
RuntimeFeature.IsDynamicCodeSupportedに基づいて選択するトリミングセーフな代替手段を用意します。aot-trimming-playbook-reference.md を参照してください。 -
抑制は最後の手段としてのみ使用する — 不変条件が証明されたリーフで、
Justificationを指定した単一の[UnconditionalSuppressMessage(...)]を使用します。#pragmaは使わないでください(IL に保持されません)。
完全なプレイブックについては、aot-trimming-playbook-reference.md を参照してください。オブジェクトオーバーロードからジェネリックオーバーロードへのリダイレクト、生成されたインターセプション用の抑制、意図的なスキャン境界、移行アナライザー、厳格モードの優先順位、一時的な警告承認ベースラインなどを掲載しています。
参照ファイル
- trimming-attributes-reference.md:属性の完全なカタログです。
RequiresUnreferencedCode、RequiresDynamicCode、DynamicallyAccessedMembers(DynamicallyAccessedMemberTypesの完全な一覧)、UnconditionalSuppressMessage、DynamicDependencyを、それぞれ意図、正確なシグネチャ、ルールとともに説明します。また、IL2xxx/IL3xxx警告コードの表も掲載しています。 - aot-trimming-playbook-reference.md:パターンプレイブックの完全版です。ソースジェネレーターとリフレクションの比較、トリミングセーフな領域と機能ガード、安全なコードと安全でないコードの分離、System.Text.Json のソース生成、
[OverloadResolutionPriority]とオプトイン方式のアナライザーを使ったオブジェクトオーバーロードからジェネリックオーバーロードへのリダイレクト、移行アナライザー、警告承認ベースライン、既知の注意点、完全な生成チェックリストを掲載しています。
生成チェックリスト(概要)
- プロジェクト — ライブラリでは
IsTrimmable/IsAotCompatible、アプリではPublishTrimmed/PublishAotを設定し、発行時の警告をゼロにします。 - リフレクションを明示する(または置き換える) — 残すリフレクションには注釈(
DynamicallyAccessedMembers/RequiresUnreferencedCode/RequiresDynamicCode)を付けます。リフレクションを使わない設計にしたい箇所に限り、ソースジェネレーター、ジェネリック(typeof(T))API、または[UnsafeAccessor]に置き換えます。 - 注釈を付ける — すべての
Type/stringソースに、最も範囲の狭いDynamicallyAccessedMembersを付けます。チェーン全体に伝播させ、安全でないエントリーポイントには[RequiresUnreferencedCode]/[RequiresDynamicCode]を付けます。 - 分離する — 安全でないリフレクション/Emit コードを境界の内側に保ちます。動的コード のパスは
RuntimeFeature.IsDynamicCodeSupportedで制御し、トリミング のパスは#ifまたは注釈で分離します。 - 抑制する — 不変条件が証明されたリーフで、正当な理由を添えて
[UnconditionalSuppressMessage]を使用する場合に限ります。#pragma/SuppressMessageは使用しないでください。 - 検証する — トリミングと AOT を有効にして発行し、アナライザーを実行して、承認済みのベースラインで警告の範囲を固定します。