更新履歴(6件・最終更新 2026年08月22日)
この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。
- 追加した図のMermaidソースの字下げを記事内の規約に統一し、レビュー指摘のあった図の表現を本文の記述に合わせて調整しました。本文の文章は変えていません。
- 本文の流れ・対比を図でも追えるように、Mermaid図を13点追加しました(地の文500〜750字につき1図の規約に合わせたものです)。既存の2図にはキャプションを追加しています。本文の文章は変えていません。
- 記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
- 外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
- 前提ツールチェーンの表を追加しました(Windowsでは「C++によるデスクトップ開発」ワークロード、各Linuxディストリビューションのインストールコマンド、macOSはXcode Command Line Tools)。Linuxでビルドしたバイナリは同じか新しいLinuxでしか動かない点も追記しています。トリミング警告の対処優先順位の表とIL2070の直す前と後の例、配布形態の選択フロー図を追加しました。あわせてWPFとWinFormsについて「慎重に見たほうがよい」としていた記述を、SDK側でトリミングのサポートが無効化されているという事実に直しました。
- 本文中の関連記事へのリンクの文言が、リンク先の現在のタイトルと食い違っていたのを、実際のタイトルに揃えました。本文の内容は変えていません。
- 初版公開
この記事を引用する(DOI: 10.5281/zenodo.21589629)
この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。
小村 豪(2026)「.NET Native AOTとは - JITやtrimmingとの違い」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21589629 https://comcomponent.com/blog/2026/03/13/001-dotnet-native-aot-what-is/
- DOI(最新版)
- 10.5281/zenodo.21589629
- DOI(この版)
- 10.5281/zenodo.21732657
すでに C# Native AOT DLLをC/C++から呼び出す方法 では、Native AOT を使って C/C++ から C# を呼ぶ話を書きました。 ただ、本当はその前に そもそも Native AOT とは何か を置いたほうが親切でした。少しだけ順番が前後しています。
Native AOT の話は、とにかく最初に用語が混ざりやすいです。
- JIT をなくす話なのか
- self-contained や single-file と何が違うのか
- ReadyToRun と同じ系統なのか
- trimming warning が大量に出るのは何が起きているのか
- WPF / WinForms / ASP.NET Core でも同じ温度感で使えるのか
このへんが一緒くたになると、Native AOT が「ただ速くなる魔法」に見えたり、逆に「制約だらけで怖いもの」に見えたりします。どちらも少し雑です。
flowchart TB
accTitle: 用語が混ざると起きる誤解
accDescr: JITやself-contained、ReadyToRun、trimmingといった用語が一緒くたになると、Native AOTがただ速くなる魔法に見えたり制約だらけで怖いものに見えたりするが、どちらも雑な見方である。
mix["用語が一緒くたになる"] --> m1["速くなる魔法に見える"]
mix --> m2["制約だらけで怖く見える"]
sort["言葉を分けて整理する"] --> fair["使いどころの見える選択肢に"]
図1: 誤解の原因は用語の混線。まず言葉を分けるところから始める。
この記事では、主に .NET 8 以降の現在の実務感を前提に、次の 4 つを先に整理します。
- Native AOT の正体
- 何がうれしくて、どこが厳しくなるのか
- ReadyToRun や trimming とどう違うのか
- どんなアプリから試すと穏やかか
目次
- まず結論(ひとことで)
- まず見る整理表
- 2.1. Native AOT まわりの言葉
- 2.2. JIT / ReadyToRun / Native AOT の違い
- Native AOT の全体像(図)
- Native AOT で何がうれしいか
- 4.1. 起動が軽くなりやすい
- 4.2. ランタイム事前インストールを前提にしなくてよい
- 4.3. 制限のある実行環境に向く
- Native AOT で何が厳しくなるか
- 5.1. リフレクションと動的コード生成
- 5.2. trimming 前提で考える必要がある
- 5.3. プラットフォームごとに発行する
- 5.4. Windows デスクトップ / COM 文脈はかなり慎重
- 最小手順
- 6.1.
csproj - 6.2. publish
- 6.3. JSON の書き方
- 6.1.
- 向いているケース
- 向かないケース
- はまりどころ
- まとめ
- 参考資料
この記事の知識マップ
.NET Native AOTは、実行時JITを使わずpublish時にネイティブコードへ事前コンパイルする発行方式で、起動時間とメモリ占有を改善しやすく、.NETランタイム未インストールの環境にも配りやすい。ReadyToRunがILを残しつつJITを一部前倒しする方向であるのに対し、Native AOTは実行時JIT自体を前提にしない分、trimmingがほぼ前提になり、自由なリフレクションや動的コード生成、built-in COMとは相性が悪くなる。WindowsのNative AOTにはbuilt-in COMが無く、WPFはtrimmingとの相性の悪さから、WinFormsはbuilt-in COM marshallingへの依存の重さから、いずれも.NET SDK側でtrimmingのサポートが無効化されており、最初のNative AOT候補としては慎重に扱うべきとされる。COMが必要な場合は、ComWrappersを前提に設計し直すことが有力な選択肢になる。
flowchart LR
accTitle: .NET Native AOTの知識マップ
accDescr: Native AOTがJITやReadyToRunとどう違い、trimmingやリフレクション・built-in COMとどのように衝突し、WPFやWinFormsがなぜ慎重な扱いになるのかを示す図
native_aot["Native AOT"]
jit_compilation["JIT(Just-In-Time)コンパイル"]
readytorun["ReadyToRun"]
trimming["trimming(トリミング)"]
reflection_dotnet["リフレクション"]
dynamic_code_generation["動的コード生成(Reflection.Emit等)"]
builtin_com_interop["built-in COM interop"]
comwrappers["ComWrappers / source-generated COM"]
wpf["WPF"]
windows_forms["Windows Forms"]
source_generator["source generator"]
system_text_json_source_gen["System.Text.Jsonのsource generation"]
self_contained_deployment["self-contained発行"]
single_file_deployment["single-file発行"]
dotnet[".NET(Core以降)"]
native_aot -.->|"両立しない"| jit_compilation
readytorun -.->|"前提とする"| jit_compilation
native_aot -->|"前提とする"| trimming
native_aot -.->|"両立しない"| reflection_dotnet
native_aot -->|"両立しない"| dynamic_code_generation
native_aot -->|"両立しない"| builtin_com_interop
comwrappers -->|"推奨される対応"| native_aot
wpf -->|"両立しない"| trimming
windows_forms -->|"前提とする"| builtin_com_interop
wpf -->|"用いるのは非推奨"| native_aot
windows_forms -->|"用いるのは非推奨"| native_aot
native_aot -->|"利用する"| source_generator
system_text_json_source_gen -->|"軽減する"| reflection_dotnet
native_aot -->|"前提とする"| self_contained_deployment
native_aot -.->|"利用する"| single_file_deployment
native_aot -->|"前提とする"| dotnet
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全16件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
1. まず結論(ひとことで)
- Native AOT は、.NET アプリを publish 時にネイティブコードへ事前コンパイルして配布する方式です。
- 実行時 JIT を使わないので、起動時間とメモリ占有が良くなりやすく、.NET ランタイム未インストール環境にも配りやすいです。
- ただし、自由なリフレクション、動的コード生成、built-in COM、trimming 非対応ライブラリとは相性が悪くなります。
- つまり、速くなる魔法というより、起動・配布・実行環境の都合のために、動的な世界を少し捨てて静的な世界へ寄せる発行モデルです。
Native AOT は「.NET をネイティブっぽく配るための仕組み」であって、単なるコンパイル高速化のチェックボックスではありません。
flowchart TB
accTitle: 何を得て何を捨てるか
accDescr: Native AOTはpublish時の事前コンパイルによって起動時間・メモリ占有・ランタイム不要の配布を得る代わりに、自由なリフレクション・動的コード生成・built-in COMとの相性を捨てる発行モデルである。
aot["Native AOT"] --> gain["得るもの"]
aot --> lose["捨てるもの"]
gain --> g1["起動とメモリと配布の軽さ"]
lose --> l1["自由なリフレクション"]
lose --> l2["動的コード生成とbuilt-in COM"]
図2: 速くなる魔法ではなく、動的な世界を少し捨てて静的な世界へ寄せる取引。
2. まず見る整理表
2.1. Native AOT まわりの言葉
最初に、このへんを分けておくと後が楽です。
| 用語 | 何をするものか | Native AOT との関係 |
|---|---|---|
| JIT | 実行時に IL からネイティブコードを作る | Native AOT はここを事前に済ませる |
| self-contained | 実行に必要な .NET 一式も一緒に配る | Native AOT はこの系統で考える |
| single-file | 配布物を 1 ファイルにまとめる | Native AOT の本質とは別ですが、結果の見た目は近くなりやすい |
| trimming | 使っていないコードを削る | Native AOT ではほぼ前提になる |
| ReadyToRun | IL を残したまま、JIT の仕事を少し前倒しする | Native AOT とは似て非なる別物 |
| source generator | 実行時の動的処理を、ビルド時コード生成へ寄せる | Native AOT と相性がよい |
混ざりやすいのは、Native AOT が 1 個の機能というより、self-contained、trimming、source generation、RID 固定 publish などと仲良く動く発行モデルであることです。
flowchart TB
accTitle: Native AOTと一緒に動く仕組み
accDescr: Native AOTは1個の機能ではなく、self-contained、trimming、source generation、RID固定publishと組み合わさって動く発行モデルであることを示す。
aot["Native AOT〔発行モデル〕"] --> s1["self-containedの系統"]
aot --> s2["trimmingはほぼ前提"]
aot --> s3["source generatorと相性良"]
aot --> s4["RID固定でpublish"]
図3: Native AOTは単独機能ではなく、周辺の仕組みとセットで動く発行モデル。
2.2. JIT / ReadyToRun / Native AOT の違い
ここも最初に 1 枚で見たほうが早いです。
| 観点 | ふつうの JIT 実行 | ReadyToRun | Native AOT |
|---|---|---|---|
| 実行時 JIT | 使う | まだ使う場面がある | 使わない |
| 配布物の中身 | IL 中心 | IL + 事前生成コード | ネイティブ実行ファイル中心 |
| 起動 | 基準 | 改善しやすい | かなり改善しやすい |
| 互換性 | いちばん広い | 広い | 制約が強い |
| 動的機能 | 使いやすい | だいたい使いやすい | 制限が多い |
| 向いている目的 | ふつうの .NET 開発全般 | まずは起動改善したい | 起動・配布・制限環境を強く取りにいく |
ReadyToRun が「JIT を少し楽にする」方向だとすれば、Native AOT は「実行時 JIT 自体を前提にしない」方向です。 同じ AOT という言葉が付いていても、温度感はかなり違います。
flowchart TB
accTitle: ReadyToRunとNative AOTの方向の違い
accDescr: ReadyToRunはILを残したままJITの仕事を少し前倒しして楽にする方向で、Native AOTは実行時JIT自体を前提にしない方向であり、同じAOTという言葉でも温度感がかなり違う。
q{"JITをどうしたいか"}
q -->|"少し楽にする"| r2r["ReadyToRun〔ILは残る〕"]
q -->|"前提にしない"| aot["Native AOT〔ネイティブ中心〕"]
r2r --> soft["互換性は広いまま"]
aot --> hard["起動は速いが制約は強い"]
図4: 同じAOTでも、「JITを楽にする」と「JITを前提にしない」では別物。
そのうえで、「では結局どの配布形態を選ぶか」を 1 枚にすると、こうなります。
flowchart TD
S["配布形態を決めたい"] --> Q1{"配布先に .NET ランタイムを入れられる?"}
Q1 -- "入れられる" --> Q2{"起動時間を詰めたい?"}
Q2 -- "そこまでではない" --> P1["framework-dependent<br/>ふつうの発行"]
Q2 -- "詰めたい" --> P2["ReadyToRun<br/>互換性を保ったまま起動改善"]
Q1 -- "入れたくない / 入れられない" --> Q3{"reflection・動的コード生成・built-in COM に依存する?"}
Q3 -- "依存する" --> P3["self-contained<br/>必要なら single-file でまとめる"]
Q3 -- "依存しない" --> Q4{"対象は console / worker / 小さな API?"}
Q4 -- "はい" --> P4["Native AOT"]
Q4 -- "いいえ" --> P5["まず self-contained<br/>動的な依存を減らしてから再検討"]
図5: 配布形態の選び方。ランタイムを置けるか、動的な仕組みを減らせるかの順に分岐する。
最初の分岐は ランタイムを配布先に置けるかどうか、次の分岐は 動的な仕組みをどこまで減らせるか です。 self-contained と single-file は「配り方」の話、ReadyToRun と Native AOT は「いつネイティブコードを作るか」の話なので、実際には組み合わせて考えることになります。
3. Native AOT の全体像(図)
Native AOT をざっくり図にすると、こうです。
flowchart LR
Src["C# / .NET ソースコード"] --> IL["IL アセンブリ"]
IL -->|通常実行| JIT["実行時 JIT"]
JIT --> Run1["アプリ実行"]
IL -->|dotnet publish + PublishAot| Analyze["AOT / trim 解析"]
Analyze --> Trim["不要コードの削減"]
Trim --> AOT["ネイティブコード生成"]
AOT --> Run2["RID 固有の実行ファイル"]
図6: 通常実行は実行時にJITするが、Native AOTは解析・削減・ネイティブ生成をpublish時に前倒しする。
普段の .NET は、まず IL を作って、実行時に必要な分だけ JIT します。 Native AOT は、その後段の大きな部分を publish 時に前倒しします。
このとき大事なのは、publish 時点で「実行時に必要になるコードを、ほぼ全部知っている必要がある」ことです。 ここで、許される書き方の前提が変わります。
- 実行時に型を見つける
- 実行時にコードを生やす
- 実行時に Assembly を読み込む
- 実行時に「まあ何とかなるでしょう」で遅延解決する
こういう書き方は、Native AOT と急に相性が悪くなります。
flowchart TB
accTitle: publish時に全部知っている必要がある
accDescr: Native AOTはpublish時点で実行時に必要になるコードをほぼ全部知っている必要があり、実行時に型を見つける・コードを生やす・Assemblyを読み込む・遅延解決するといった書き方と相性が悪くなる。
need["publish時に必要コードを確定"] --> ng1["実行時に型を見つける"]
need --> ng2["実行時にコードを生やす"]
need --> ng3["実行時にAssemblyを読む"]
need --> ng4["遅延解決でしのぐ"]
ng1 -.-> bad["どれもAOTと相性が悪い"]
図7: 前提が変わる核心。「実行時に決める」書き方ほどNative AOTと衝突する。
4. Native AOT で何がうれしいか
4.1. 起動が軽くなりやすい
Native AOT のいちばん分かりやすい効き目は、やはり起動です。
- CLI ツール
- 短命プロセス
- サーバーレスっぽい起動
- コンテナの起動・入れ替え
- 監視ツールや小さな常駐プロセス
このへんでは JIT コストが見えやすく、Native AOT でそこを前倒しできる分、初動が軽くなります。
メモリ占有も改善しやすいので、同じ台数を詰め込みたい場面でも有利です。 特に、同じプロセスが大量に立つクラウド側では、この差がじわじわ効きます。
4.2. ランタイム事前インストールを前提にしなくてよい
Native AOT で publish したアプリは、.NET ランタイム未インストール環境でも動かしやすくなります。
これは地味に大きいです。
- 配布先に「.NET 9 Runtime を先に入れてください」と言いたくない
- コンテナイメージを細くしたい
- 小さいツールを 1 本だけ置いて動かしたい
- 実行環境に JIT を許したくない / 許されない
こういう場面では、「ランタイムを別途そろえる前提」がないだけで、かなり話が静かになります。
ここでいう「ランタイム不要」は、配布先に別途 .NET を入れなくてよいという意味です。 アプリの中に必要な runtime 相当部分まで完全に消える、という話ではありません。
flowchart TB
accTitle: ランタイム不要の正しい意味
accDescr: Native AOTのランタイム不要とは配布先に別途.NETを入れなくてよいという意味であり、アプリの中から必要なruntime相当部分まで完全に消えるという意味ではない。
word["ランタイム不要という言葉"] --> ok["配布先に.NETを入れなくてよい"]
word -.-> ngx["runtime相当が消える訳ではない"]
ok --> merit["配布と起動の前提が減る"]
図8: 「ランタイム不要」は配布先の話。runtime相当はアプリの中に含まれている。
4.3. 制限のある実行環境に向く
Native AOT は、実行時 JIT を使わないので、JIT が許されない環境でも動かしやすいです。
ここは desktop よりも、クラウド、コンテナ、モバイル寄りの話で効きやすいです。 ただ、Windows 開発の文脈でも、「配布先で余計な前提を減らしたい」という意味では十分うれしい点です。
5. Native AOT で何が厳しくなるか
5.1. リフレクションと動的コード生成
Native AOT の本丸の制約はここです。
Assembly.LoadFileのような動的読み込みSystem.Reflection.Emitのような実行時コード生成- 実行時に無制限に型をたどるリフレクション
- 実行時に generic を好き放題組み立てるような書き方
このへんは、publish 時に必要コードを確定しにくいので、AOT warning の温床になります。
もちろん、リフレクションが 1 行でもあれば即アウト、というほど単純ではありません。 ただ、「実行時に見て決める」寄りの設計ほど厳しくなる、この傾向は外れません。
warning 名でよく出てくるのは RequiresDynamicCode 系です。
これは「その呼び出しは AOT で壊れるかもしれない」という意味なので、雑に suppress しないほうが安全です。
Native AOT をやるときは、「実行時の賢さ」を減らして、「ビルド時の明示」を増やす方向へ寄せる、と思っておくと分かりやすいです。
flowchart TB
accTitle: 実行時の賢さからビルド時の明示へ
accDescr: 動的読み込みや実行時コード生成、無制限のリフレクションはAOT warningの温床になるため、実行時の賢さを減らしてビルド時の明示を増やす方向へ設計を寄せる。
dynamicway["実行時の賢さに頼る設計"] --> warn["RequiresDynamicCode系のwarning"]
warn --> shift["ビルド時の明示を増やす"]
shift --> calm["publish時に必要コードが確定"]
warn -.-> nosup["雑にsuppressしない"]
図9: 本丸の制約への向き合い方は一方向。「実行時に見て決める」を「ビルド時に明示する」へ。
5.2. trimming 前提で考える必要がある
Native AOT は trimming と深く結びついています。 ここで見落としやすいのが、自分のコードだけでなく、依存ライブラリ側の書き方も効く ことです。
要注意なのは、このあたりです。
- reflection ベースのシリアライザー
- 実行時スキャンで型を集める DI / プラグイン構成
- 文字列名から型を探して実体化する仕組み
- 動的 proxy や IL 生成に寄るライブラリ
ここで warning が出ているのに「publish は通ったからヨシ」とすると、あとでかなり渋いです。 Native AOT では、warning はだいたい本気で読んだほうがよいです。
flowchart TB
accTitle: 自分のコードだけでは決まらないtrimming
accDescr: trimmingでは自分のコードだけでなく依存ライブラリ側の書き方も効き、reflectionベースのシリアライザーや実行時スキャンのDI、文字列名からの型解決、動的proxyに寄るライブラリが要注意になる。
trim["trimmingの成否"] --> own["自分のコードの書き方"]
trim --> dep["依存ライブラリの書き方"]
dep -.-> ex["reflection系や実行時スキャン系"]
dep --> care["warningを本気で読んで確かめる"]
図10: 見落としやすいのは依存ライブラリ側。「publishが通ったからヨシ」は後で渋くなる。
とはいえ「本気で読め」だけだと動けないので、対処の優先順位を書いておきます。 公式ドキュメントも、この順で試すことを勧めています。
| 順番 | 対処 | 使う場面 |
|---|---|---|
| 1 | reflection をやめる | Activator.CreateInstance(Type) を generic 引数に置き換えられる、source generator に寄せられる |
| 2 | DynamicallyAccessedMembers を付ける |
reflection は必要だが、対象の型はコンパイル時に分かっている |
| 3 | RequiresUnreferencedCode を付ける |
型名を実行時の文字列で決めるなど、本質的に静的解析できない |
| 4 | UnconditionalSuppressMessage で抑止する |
上記が全部無理で、安全だと確認できたときの最後の手段 |
第一歩としていちばん多いのは、2 の「型は分かっているのに、警告だけ出ている」パターンです。
たとえば、次のコードは IL2070 が出ます。
// IL2070: 'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods'
void PrintMethodNames(Type type)
{
foreach (var method in type.GetMethods())
{
Console.WriteLine(method.Name);
}
}
GetMethods() を呼ぶなら public メソッドを残してほしい、と属性で宣言すれば消えます。
using System.Diagnostics.CodeAnalysis;
void PrintMethodNames(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type type)
{
foreach (var method in type.GetMethods())
{
Console.WriteLine(method.Name);
}
}
// 呼び出し側が typeof で渡していれば、要求は自動的に満たされます
PrintMethodNames(typeof(DateTime));
呼ぶ API と必要な指定はだいたい対応しています。GetMethod / GetMethods なら PublicMethods、GetProperty / GetProperties なら PublicProperties、Activator.CreateInstance なら PublicParameterlessConstructor か PublicConstructors です。
DynamicallyAccessedMemberTypes.All は楽ですが、対象型のメンバーをまるごと残してサイズが膨らみますし、残したメンバーが別の warning を連れてくるので、必要最小限を指定する のが原則です。
なお、属性を足しても warning が消えないときは、reflection を使っている場所から呼び出し元へさかのぼって、経路の全部に付いているか を確認します。途中で 1 か所抜けていると、そこで要求が途切れます。
flowchart TB
accTitle: 属性を足してもwarningが消えないとき
accDescr: DynamicallyAccessedMembersは必要最小限を指定し、warningが消えないときはreflectionを使っている場所から呼び出し元へさかのぼって経路の全部に属性が付いているかを確認する。1か所抜けるとそこで要求が途切れる。
warnleft["属性を足したのにwarningが残る"] --> back["呼び出し元へさかのぼる"]
back --> chain["経路の全部に付いているか確認"]
chain -.-> cut["1か所抜けると要求が途切れる"]
warnleft -.-> minrule["指定は必要最小限が原則"]
図11: 属性は点ではなく経路に付ける。途中で1か所抜けると要求はそこで途切れる。
5.3. プラットフォームごとに発行する
Native AOT は RID(Runtime Identifier)固定で publish します。
つまり、win-x64 用に作ったものを、そのまま linux-x64 で動かす、という世界ではありません。
- Windows x64
- Windows Arm64
- Linux x64
- Linux Arm64
- macOS Arm64
のように、ターゲットごとに発行物を作る前提です。
ここは、ふつうの framework-dependent な .NET より、ぐっと「ネイティブアプリっぽい」感覚になります。
5.4. Windows デスクトップ / COM 文脈はかなり慎重
KomuraSoft の文脈だと、ここは特に重要です。
Windows では Native AOT に built-in COM がありません。 さらに、WPF は trimming と相性が悪く、WinForms は built-in COM marshalling への依存が重いため、少なくとも現時点では、どちらも「最初の Native AOT 候補」としてはかなり慎重に見たほうがよいです。
ここでいう「現時点」の基準を書いておきます。 この記事の記述は、.NET 8 / 9 / 10 世代の公式ドキュメント(2026 年 7 月時点)に基づきます。 WPF と WinForms については、Microsoft の「Known trimming incompatibilities」で、WPF は reflection と実行時のコード検査への依存が強く trimming 後にほぼ動かないため、WinForms は built-in COM marshalling への依存が重いため、どちらも .NET SDK 側で trimming サポートが無効化されている と明記されています。 つまり「慎重に見たほうがよい」ではなく、現状は SDK が止めている状態です。将来この記述が変わる可能性はあるので、読む時点で同じページを確認してください。
flowchart TB
accTitle: WPFとWinFormsが止められている理由
accDescr: WindowsのNative AOTにはbuilt-in COMが無く、WPFはreflectionと実行時コード検査への依存でtrimming後にほぼ動かず、WinFormsはbuilt-in COM marshallingへの依存が重いため、どちらも.NET SDK側でtrimmingサポートが無効化されている。
win["WindowsのNative AOT"] --> nocom["built-in COMが無い"]
wpf["WPF"] --> refl["reflection依存が強い"]
wf["WinForms"] --> commar["COM marshalling依存が重い"]
refl --> off["SDK側でtrimmingが無効化"]
commar --> off
図12: 「慎重に」ではなく、現状はSDKが止めている。デスクトップ本体は最初の候補にしない。
要するに、
- WPF / WinForms 本体をいきなり Native AOT 化する
- COM interop を普通の感覚でそのまま持ち込む
このへんは、publish 時の warning と実行時の制約が一気に増えて、対応コストが跳ね上がりやすいです。
逆に、
- コンソール
- worker
- 小さな Web API
- ネイティブ連携の中でも C の関数境界に寄せやすい部品
のほうが入り口として素直です。
COM が必要なら、JIT のままにするか、ComWrappers / source-generated COM を前提に設計し直すほうが筋がよい場面もあります。
6. 最小手順
その前に、Native AOT の publish にはネイティブのツールチェーンが要ります。
PublishAot を付けただけで dotnet publish を打つと、コンパイルではなく最後のネイティブリンクで落ちます。ここが最初の関門なので、先に入れておいてください。
| 環境 | 必要なもの |
|---|---|
| Windows | Visual Studio 2022 以降。「C++ によるデスクトップ開発」ワークロードを、既定のコンポーネントを全部入れた状態で入れる |
| Ubuntu 18.04 以降 | sudo apt-get install clang zlib1g-dev |
| Alpine 3.15 以降 | sudo apk add clang build-base zlib-dev |
| Fedora 39 以降 / RHEL 8 以降 | sudo dnf install clang zlib-ng-devel zlib-ng-compat-devel zlib-devel |
| macOS | Xcode の Command Line Tools(.NET 8 以降で対応) |
要するに、コンパイラツールチェーンと、.NET ランタイムが依存するライブラリの開発用パッケージです。 CI で回すなら、dotnet/samples の Native AOT サンプルに Linux / Windows 両方の Dockerfile があるので、そこから前提のインストール手順を持ってくるのが早いです。
なお、Linux でビルドしたバイナリは 同じか、それより新しい Linux でしか動きません。Ubuntu 20.04 で作ったものは 20.04 以降では動きますが、18.04 では動きません。ビルド環境の選び方がそのまま配布可能範囲になります。
flowchart TB
accTitle: publish前の関門と配布可能範囲
accDescr: Native AOTのpublishにはネイティブツールチェーンが必要で、無いと最後のネイティブリンクで落ちる。またLinuxでビルドしたバイナリは同じかそれより新しいLinuxでしか動かず、ビルド環境の選び方が配布可能範囲になる。
tool{"ツールチェーンはあるか"}
tool -->|"ない"| fail["ネイティブリンクで落ちる"]
tool -->|"ある"| bin["RIDごとのバイナリが出る"]
bin -.-> range["Linuxは同じか新しい環境でのみ動く"]
range -.-> pick["ビルド環境の選び方が配布範囲"]
図13: 最初の関門はツールチェーン。Linuxではビルド環境の古さが配布可能範囲を決める。
6.1. csproj
まずはプロジェクトファイルに PublishAot を入れます。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<PublishAot>true</PublishAot>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
サンプルは net8.0 で十分です。考え方自体は .NET 9 / 10 でもほぼ同じです。
大事なのは、dotnet publish のコマンドラインにだけ一時的に付けるより、
ふだんからプロジェクトに置いて、build / publish 時の解析を日常的に見る ことです。
なお、<PublishAot>true</PublishAot> を入れても、普段のローカル実行までいきなり Native AOT になるわけではありません。
日常の dotnet run や通常実行は JIT で、Native AOT コンパイルの本番は publish 時です。
flowchart TB
accTitle: PublishAotを入れたあとの日常
accDescr: PublishAotをプロジェクトに置いても日常のdotnet runはJITのままで、Native AOTコンパイルの本番はpublish時に走る。ふだんからプロジェクトに置いてbuildとpublish時の解析を日常的に見るのが大事。
put["csprojにPublishAotを置く"] --> daily["日常のdotnet runはJITのまま"]
put --> pub["publish時にAOTコンパイル"]
daily -.-> watch["解析とwarningを日常的に見る"]
図14: 日常はJIT、本番はpublish。だからこそ設定は一時付けでなく常置して解析を見続ける。
6.2. publish
たとえば Windows x64 向けなら、こんな形です。
dotnet publish -c Release -r win-x64
Linux x64 向けなら、こうです。
dotnet publish -c Release -r linux-x64
出力は RID 固定です。 「1 本でどこでも動く .NET DLL」より、「その OS / アーキテクチャ向けに作った実行ファイル」という見方に変わります。
Web API 側から触るなら、Native AOT 前提のテンプレートから入るのが楽です。
dotnet new webapiaot -o MyFirstAotWebApi
worker なら、こちらです。
dotnet new worker -o WorkerWithAot --aot
6.3. JSON の書き方
Native AOT で地味によく当たるのが JSON です。
System.Text.Json は普段の感覚で使うと reflection へ寄りやすいので、source generation に寄せたほうが穏やかです。
using System.Text.Json;
using System.Text.Json.Serialization;
[JsonSerializable(typeof(AppConfig))]
internal partial class AppJsonContext : JsonSerializerContext
{
}
public sealed class AppConfig
{
public string? Name { get; init; }
public int RetryCount { get; init; }
}
var config = new AppConfig
{
Name = "sample",
RetryCount = 3
};
string json = JsonSerializer.Serialize(config, AppJsonContext.Default.AppConfig);
実務では、Native AOT 対応というより、「実行時に型を探させない」方向へ寄せる と覚えておくと外しにくいです。
7. 向いているケース
Native AOT が気持ちよくハマりやすいのは、こういうケースです。
- 起動が主役の CLI / ツール
- コンテナで大量配備する小さな API
- worker / バックグラウンドサービス
- サーバーレスや短命プロセス
- ネイティブアプリに差し込む小さな .NET 部品
- 実行環境に .NET ランタイムの事前インストールを求めたくない場面
共通しているのは、境界が比較的はっきりしていて、動的な仕組みを減らしやすい ことです。
8. 向かないケース
逆に、最初から Native AOT を主戦場にしないほうがよい場面もはっきりあります。
- WPF / WinForms の既存大規模アプリ本体
- built-in COM interop 前提の構成
- 実行時 plugin 読み込みが主役のアプリ
- reflection で型探索するフレームワーク依存が強い構成
System.Reflection.Emitや動的 proxy を当然のように使うライブラリ- C++/CLI を挟んだ設計
このへんは、JIT 前提のふつうの .NET、あるいは ReadyToRun、あるいは設計の切り分け見直しのほうが筋がよいです。
9. はまりどころ
最後に、Native AOT 初手で踏みやすい点をまとめます。
- publish warning を軽く見る
- 前述のとおり、Native AOT の warning は本気で読む対象です。
- build は通るのに publish で壊れる
- publish 時に依存ライブラリまで含めた解析が本気で走るので、ここで初めて見えるものがあります。
- ReadyToRun と Native AOT を同じ気分で扱う
- 似た言葉ですが、制約の強さがかなり違います。
- desktop アプリ本体からいきなり始める
- まずは console / worker / 小さな API のほうが穏やかです。
- JSON や設定バインドを普段のノリで書く
- reflection 前提の書き方は、あとで効いてきます。
- プラットフォーム非依存のつもりで配る
- Native AOT の配布物は RID 固定です。
- 「Native AOT = なんでも速くなる」と思う
- 主役は起動、配布、実行環境です。ここを外すと期待値がずれます。
Native AOT では、可否を最終的に決めるのは dotnet build ではなく dotnet publish です。
ここを早めに回し始めると、後半で困りにくくなります。
flowchart TB
accTitle: 可否を決めるのはpublish
accDescr: buildは通るのにpublishで壊れることがあるのは、publish時に依存ライブラリまで含めた解析が本気で走るからで、Native AOTの可否を最終的に決めるのはdotnet buildではなくdotnet publishである。
buildok["dotnet buildは通る"] --> notyet["まだ可否は決まっていない"]
pubx["dotnet publish"] --> deep["依存込みの解析が本気で走る"]
deep --> verdict["ここで初めて可否が見える"]
verdict -.-> early["だから早めにpublishを回す"]
図15: はまりどころの共通項。「buildが通った」で安心せず、publishを早めに回す。
10. まとめ
Native AOT をひとことで言うと、 .NET アプリを、動的な実行モデルから、静的に確定しやすい配布モデルへ寄せる仕組み です。
見ておきたいポイントは、この 5 つに尽きます。
- Native AOT は publish 時にネイティブコードへ事前コンパイルする
- 起動、メモリ、配布の都合にはよく効く
- その代わり、reflection、動的コード生成、built-in COM、trimming 非対応コードには厳しい
- 最初の対象は、desktop 本体より console / worker / 小さな API のほうが穏やか
- warning を消しながら、publish ベースで早めに確かめるのが大事
Native AOT は、全部の .NET アプリに付ける標準スイッチではありません。 ですが、起動が大事、配布を軽くしたい、実行環境の前提を減らしたい という場面では、かなり強い武器です。
逆に、WPF / WinForms / COM の濃い世界では、まだ普通の .NET のほうが筋がよい場面も多いです。 ここを見分けられると、Native AOT は「難しい新機能」ではなく、使いどころのはっきりした選択肢になります。
11. 参考資料
- Native AOT deployment overview - .NET
- Native AOT deployment overview - .NET (日本語)
- Introduction to AOT warnings - .NET
- Fixing trim warnings - .NET
- DynamicallyAccessedMembersAttribute Class - .NET
- Prepare .NET libraries for trimming - .NET
- Known trimming incompatibilities - .NET
- How to use source generation in System.Text.Json - .NET
- ASP.NET Core support for Native AOT
- ReadyToRun deployment overview - .NET
- Building native libraries - .NET
- ComWrappers source generation - .NET
- 関連記事: C# Native AOT DLLをC/C++から呼び出す方法
- 関連記事: C#からネイティブDLLを呼ぶ:C++/CLIラッパー vs P/Invoke
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
マルチスレッドの実務ベストプラクティス .NET編 ── スレッドを増やす前に決めておくこと
「スレッドを立てたら、たまに落ちる・固まる」を防ぐ設計の定石を.NET/C#向けに整理。スレッドを自分で作らずTaskに乗る、共有可変状態を減らす、ロックの規律、CancellationTokenでの停止設計、UIスレッドの扱いまで解説します。
業務システムのコード設計 ── 商品コード・顧客コードの決め方とチェックディジット
商品コード・顧客コードなど業務システムのコード体系を決める実践ガイド。有意コードと無意味連番の判断表、JAN・Luhn等のチェックディジット算式とC#実装、Excelの0落ち対策、桁あふれと移行まで整理します。
Windowsのセッション分離をどう理解するか ── Session 0・RDP・複数ユーザー同時実行
Windowsアプリ開発者が混乱しがちな「セッション」の概念を整理します。サービスがUIを出せないSession 0分離の理由、RDP接続時のセッションの挙動、名前付きオブジェクトのセッション分離、共有PC・RDS環境でありがちな設計ミスまでを実務目線で解説します。
Windowsのプロセス間通信をどう選ぶか ── 名前付きパイプ / TCP / gRPC / 共有メモリ / COM 判断表
Windowsアプリ同士の連携手段をどう選ぶか。名前付きパイプ、ローカルTCP、gRPC、共有メモリ、ファイル連携、COMの得意分野と落とし穴を判断表で整理し、定番構成と名前付きパイプの実装例まで解説します。
Windowsアプリのデータ保存先の選び方 ── SQLite / JSON / レジストリ / Access 判断表
Windowsデスクトップアプリのデータをどこに・何で保存するか。AppData/ProgramDataの使い分け、SQLite・JSONファイル・レジストリ・Access(.accdb)それぞれの得意分野と落とし穴を判断表つきで整理し、破損対策やビット数問題まで実務目線で...
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
32bit / 64bit テーマ
32bit / 64bit、ネイティブ連携、C++/CLI まわりを整理するトピックです。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
業務アプリ、装置連携、通信ツールなどの Windows ソフト開発を支援します。
技術相談・設計レビュー
改修方針、設計レビュー、既存資産の扱いを整理するための技術相談です。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- Native AOTとは何ですか?
- .NET アプリを publish 時にネイティブコードへ事前コンパイルして配布する発行方式です。実行時 JIT を使わないため、起動時間とメモリ占有が良くなりやすく、.NET ランタイム未インストールの環境にも配りやすくなります。その代わり、自由なリフレクション、動的コード生成、built-in COM、trimming 非対応ライブラリとは相性が悪くなります。速くなる魔法というより、起動・配布・実行環境の都合のために、動的な世界を少し捨てて静的な世界へ寄せる発行モデルです。
- Native AOTとReadyToRunは何が違いますか?
- ReadyToRun は IL を残したまま JIT の仕事を少し前倒しする方式で、実行時 JIT をまだ使う場面があります。互換性は広く、動的機能もだいたい使いやすいままです。一方 Native AOT は実行時 JIT 自体を前提にせず、配布物はネイティブ実行ファイル中心になり、起動はかなり改善しやすい代わりに制約が強くなります。同じ AOT という言葉が付いていても、ReadyToRun が「JIT を少し楽にする」方向、Native AOT が「JIT を前提にしない」方向で、温度感はかなり違います。
- WPFやWinFormsのアプリでNative AOTは使えますか?
- 現時点ではかなり慎重に見たほうがよいです。Windows の Native AOT には built-in COM がなく、WPF は trimming と相性が悪く、WinForms は built-in COM marshalling への依存が重いため、どちらも最初の Native AOT 候補には向きません。COM が必要なら、JIT のままにするか、ComWrappers / source-generated COM を前提に設計し直すほうが筋がよい場面もあります。入り口としては、コンソール、worker、小さな Web API のほうが素直です。
- Native AOTはどんなアプリに向いていますか?
- 起動が主役の CLI ツール、コンテナで大量配備する小さな API、worker やバックグラウンドサービス、サーバーレスや短命プロセス、ネイティブアプリに差し込む小さな .NET 部品、実行環境に .NET ランタイムの事前インストールを求めたくない場面に向いています。共通するのは、境界が比較的はっきりしていて動的な仕組みを減らしやすいことです。逆に、実行時プラグイン読み込みが主役のアプリや、リフレクションで型探索するフレームワーク依存が強い構成には向きません。