C#をネイティブコンパイルする方法とは?AOTの仕組み・メリット・注意点を初心者向けに解説

はじめに

C#は、Webアプリ、業務システム、CLIツール、デスクトップアプリ、ゲーム開発など幅広い分野で使われているプログラミング言語です。通常、C#で作成したプログラムは.NETの実行環境上で動きますが、近年は「C#をネイティブコンパイルして配布したい」「AOTを使って起動を速くしたい」と考えるケースが増えています。

特に、コマンドラインツール、コンテナ環境、サーバーレス、軽量なAPIなどでは、起動時間やメモリ使用量が重視されます。そのような場面で注目されるのが、.NETの「Native AOT」です。

Native AOTを使うと、C#のアプリケーションを事前にネイティブコードへコンパイルし、実行時にJITコンパイルを行わない形で配布できます。Microsoftの公式ドキュメントでも、Native AOTアプリは起動時間の短縮、メモリ使用量の削減、.NETランタイムがインストールされていない環境での実行といった特徴が説明されています。Microsoft Learn

この記事では、「C#のネイティブコンパイルとは何か」「AOTやJITと何が違うのか」「Native AOTを使って実行ファイルを作るにはどうすればよいのか」を、初心者にもわかりやすく解説します。

1. C#のネイティブコンパイルとは?

1-1. C#でいう「ネイティブコンパイル」の意味

C#におけるネイティブコンパイルとは、C#で書いたプログラムを最終的にOSやCPUが直接実行しやすいネイティブコードへ変換することを指します。

ここでいう「ネイティブコード」とは、Windows、Linux、macOSなどの特定の実行環境に合わせた機械語に近い形式のコードです。たとえば、Windows x64向け、Linux x64向け、macOS Arm64向けといったように、対象となるOSやCPUアーキテクチャに依存します。

通常のC#アプリケーションは、まずILという中間言語にコンパイルされ、実行時に.NETランタイムによって処理されます。一方、Native AOTを使ったネイティブコンパイルでは、発行時にILをネイティブコードへ変換します。そのため、アプリ実行時にJITコンパイラを使わない点が大きな特徴です。Microsoft Learn

1-2. 通常のC#実行方式とネイティブコンパイルの違い

通常のC#アプリケーションでは、開発者が書いたC#コードは一度ILに変換されます。ILは、特定のCPUに直接依存しない中間的なコードです。アプリを起動すると、.NETランタイムがILを読み込み、必要に応じてJITコンパイルを行い、実行環境に合ったネイティブコードへ変換します。

これに対して、ネイティブコンパイルでは、アプリの発行時点でネイティブコード化を行います。実行時にJITコンパイルする工程を減らせるため、起動時間の短縮が期待できます。

ただし、ネイティブコンパイルは万能ではありません。実行時にコードを生成する仕組みや、リフレクションを多用する処理とは相性が悪い場合があります。通常のJIT実行では問題なく動くコードでも、Native AOTでは警告やエラーになることがあります。

1-3. JIT・AOT・Native AOTの関係

C#の実行方式を理解するうえで重要なのが、JIT、AOT、Native AOTの違いです。

JITは「Just-In-Time」の略で、実行時に必要なコードをネイティブコードへコンパイルする方式です。C#の通常の実行方式では、このJITコンパイルが広く使われています。

AOTは「Ahead-Of-Time」の略で、実行前にあらかじめコンパイルしておく方式です。実行時のコンパイル処理を減らせるため、起動時間や実行環境の制約に対して有利になることがあります。

Native AOTは、.NETにおけるAOTの一種です。C#アプリケーションを発行時にネイティブコードへコンパイルし、自己完結した実行ファイルとして配布しやすくします。Native AOTアプリは実行時にJITコンパイラを使用しないため、JITが許可されない制限環境でも使いやすい点が特徴です。Microsoft Learn

1-4. 初心者が混同しやすい「自己完結配置」との違い

C#の配布方法を調べていると、「自己完結配置」と「ネイティブコンパイル」を混同しやすいです。

自己完結配置とは、アプリの実行に必要な.NETランタイムを一緒に含めて配布する方式です。配布先のPCやサーバーに.NETランタイムがインストールされていなくても動かせるようになります。

一方、Native AOTによるネイティブコンパイルは、アプリをネイティブコードへ変換する方式です。Native AOTの発行は自己完結型の配布モデルを使いますが、「自己完結配置=必ずネイティブコンパイル」という意味ではありません。

通常の自己完結配置では、.NETランタイムやアプリのILが含まれる形になることがあります。Native AOTでは、発行時にネイティブコードへ変換され、JITに依存しない形になります。つまり、自己完結配置は「配布形態」の話であり、ネイティブコンパイルは「コンパイル方式」の話だと考えると理解しやすいです。

2. C#プログラムが実行される仕組み

2-1. C#コードがILに変換される流れ

C#で書いたコードは、そのままOSが直接実行するわけではありません。通常は、C#コンパイラによってILに変換されます。

たとえば、次のような簡単なC#コードがあるとします。

C#
Console.WriteLine("Hello, Native AOT!");

このコードはビルド時にILへ変換されます。ILは.NETの共通中間言語であり、C#だけでなくF#やVB.NETなどの.NET言語でも使われます。

ILの段階では、まだWindows x64専用、Linux Arm64専用といった完全なネイティブコードにはなっていません。実際に実行されるときに、.NETランタイムが環境に合わせて処理します。

2-2. CLRとJITコンパイルの役割

C#アプリケーションの実行には、CLRが重要な役割を持っています。CLRは「Common Language Runtime」の略で、.NETアプリケーションを実行するためのランタイム環境です。

CLRは、メモリ管理、例外処理、型安全性のチェック、ガベージコレクション、JITコンパイルなどを担当します。通常のC#アプリでは、CLRがILを読み込み、実行時に必要な部分をJITコンパイラでネイティブコードへ変換します。

JIT方式のメリットは、実行環境に合わせて最適化しやすいことです。また、動的なコード生成やリフレクションを利用するライブラリとも比較的相性がよいです。

一方で、アプリ起動時や初回実行時にコンパイル処理が発生するため、起動時間やメモリ使用量に影響する場合があります。

2-3. AOTコンパイルで何が変わるのか

AOTコンパイルでは、実行時ではなく発行時にコードをコンパイルします。Native AOTの場合、ILを発行時にネイティブコードへ変換します。

これにより、アプリ起動時にJITコンパイルを行う必要がなくなります。特に、短時間で起動して処理を終えるCLIツールや、リクエストに応じて素早く起動したいサーバーレスアプリでは効果を感じやすいです。

ただし、AOTでは「実行時に何が使われるかわからないコード」を扱いにくくなります。たとえば、リフレクションで型を探したり、実行時にコードを生成したりする処理は、事前コンパイルと相性が悪いことがあります。

2-4. ネイティブコード化すると実行ファイルはどう変わるか

Native AOTでC#アプリを発行すると、対象プラットフォーム向けの実行ファイルが生成されます。たとえば、Windows向けであれば.exe、LinuxやmacOS向けであれば実行可能なバイナリが出力されます。

通常の.NETアプリでは、DLL、runtimeconfig.json、deps.json、関連ライブラリなど複数のファイルが出力されることがあります。一方、Native AOTでは単一ファイルに近い形で配布しやすくなります。

ただし、Native AOTで生成された実行ファイルは、ターゲット環境に依存します。Windows向けにビルドしたものをLinuxでそのまま実行することはできません。Linux向け、macOS向け、Windows向けにそれぞれビルドする必要があります。

3. C#をネイティブコンパイルする主な方法

3-1. .NET Native AOTを使う方法

現在、C#をネイティブコンパイルする代表的な方法は、.NETのNative AOTを使う方法です。

Native AOTは、.NETアプリケーションを発行時にネイティブコードへコンパイルする仕組みです。プロジェクトファイルにPublishAotを設定し、dotnet publishコマンドで対象ランタイムを指定して発行します。

基本的な流れは次のとおりです。

XML
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>

そのうえで、次のように発行します。

Bash
dotnet publish -c Release -r win-x64

Microsoftの公式ドキュメントでも、<PublishAot>true</PublishAot>をプロジェクトファイルに追加し、dotnet publish -r <RID>で特定のランタイム向けに発行する手順が示されています。Microsoft Learn

3-2. dotnet publishでネイティブ実行ファイルを作成する方法

C#のNative AOT発行では、dotnet publishコマンドを使います。

たとえば、Windows x64向けに発行する場合は次のようにします。

Bash
dotnet publish -c Release -r win-x64

Linux x64向けであれば、次のようにします。

Bash
dotnet publish -c Release -r linux-x64

macOS Arm64向けであれば、次のように指定します。

Bash
dotnet publish -c Release -r osx-arm64

-c ReleaseはRelease構成でビルドする指定です。Native AOTでは、最適化された発行結果を確認するためにもReleaseビルドで試すのが基本です。

-rはRuntime Identifier、つまり対象ランタイムを指定するオプションです。Windows x64、Linux x64、macOS Arm64など、配布先に合わせたRIDを指定します。

3-3. Windows・Linux・macOSでの違い

Native AOTは、対象OSごとに必要なビルド環境が異なります。

Windowsでは、Visual Studio 2022以降と、C++デスクトップ開発ワークロードなどのネイティブツールチェーンが必要になります。Linuxでは、clangやzlib関連の開発パッケージが必要になる場合があります。Ubuntuでは、公式ドキュメントでclangzlib1g-devのインストール例が示されています。macOSでは、Xcode Command Line Toolsが必要です。Microsoft Learn

また、Linuxで作成したNative AOTバイナリは、同じか新しいLinuxバージョンで動作する想定です。たとえば、Ubuntu 20.04で作成したバイナリはUbuntu 20.04以降では動きますが、Ubuntu 18.04では動かない可能性があると説明されています。Microsoft Learn

3-4. Blazor WebAssemblyやiOSなどAOTが使われるケース

C#のAOTは、Native AOTだけで使われるわけではありません。

Blazor WebAssemblyでは、.NETコードをWebAssemblyへAOTコンパイルできます。これにより、実行時パフォーマンスの向上が期待できますが、アプリサイズが大きくなる傾向があります。Microsoft Learn

iOSやMac CatalystのArm64ビルドでもAOTが重要です。Appleのセキュリティ制限により、動的に生成したコードを実行できない環境があるため、iOSやMac CatalystではAOTコンパイルが使われます。Microsoft Learn

つまり、AOTは「C#を高速化するためだけの機能」ではなく、実行環境の制約に対応するためにも使われます。

3-5. 過去の.NET Nativeとの違い

「Native AOT」と似た言葉に「.NET Native」があります。.NET Nativeは、主にUWPアプリで使われていたレガシーなプリコンパイル技術です。

現在のモダンな.NETでは、Native AOTが使われます。Microsoftのドキュメントでも、.NET Nativeは引き続きセキュリティと信頼性の修正を受けるものの、新機能の更新は受け取らないと説明されています。Microsoft Learn

そのため、これからC#のネイティブコンパイルを学ぶなら、基本的にはNative AOTを中心に理解するのがおすすめです。

4. Native AOTでC#をネイティブコンパイルする手順

4-1. 必要な.NET SDKと開発環境を準備する

まず、Native AOTに対応した.NET SDKをインストールします。Native AOTの利用やAOT互換性の警告を適切に扱うには、少なくともnet8.0以降を対象にするのが基本です。公式ドキュメントでも、AOT互換性の分析警告には最低でもnet8.0以降が必要とされています。Microsoft Learn

確認コマンドは次のとおりです。

Bash
dotnet --version

次に、対象OSに応じたネイティブビルドツールを準備します。

Windowsの場合は、Visual Studio 2022以降とC++デスクトップ開発ワークロードを用意します。

Ubuntuの場合は、次のように必要なパッケージをインストールします。

Bash
sudo apt-get install clang zlib1g-dev

macOSの場合は、Xcode Command Line Toolsをインストールします。

Bash
xcode-select --install

4-2. プロジェクトファイルにPublishAotを設定する

次に、C#プロジェクトの.csprojファイルを編集します。

たとえば、コンソールアプリの場合は次のような設定になります。

XML
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
</PropertyGroup>

</Project>

重要なのは、<PublishAot>true</PublishAot>です。この設定により、発行時にNative AOTコンパイルが有効になります。

PublishAotはコマンドラインで指定することもできますが、プロジェクトファイルに書くのが一般的です。公式ドキュメントでも、発行時以外の分析にも関係するため、コマンドラインよりプロジェクトファイルに設定するほうが望ましいとされています。Microsoft Learn

4-3. dotnet publishコマンドを実行する

プロジェクトファイルを設定したら、dotnet publishを実行します。

Windows x64向けの場合は次のとおりです。

Bash
dotnet publish -c Release -r win-x64

Linux x64向けの場合は次のとおりです。

Bash
dotnet publish -c Release -r linux-x64

macOS Arm64向けの場合は次のとおりです。

Bash
dotnet publish -c Release -r osx-arm64

また、コマンドラインでPublishAotを指定する場合は、次のようにも書けます。

Bash
dotnet publish -c Release -r win-x64 -p:PublishAot=true

ただし、継続的にNative AOTを使うなら、.csprojPublishAotを書いておくほうが管理しやすいです。

4-4. 生成された実行ファイルを確認する

発行が成功すると、通常は次のようなディレクトリに成果物が出力されます。

bin/Release/net8.0/win-x64/publish/

Windows向けであれば、アプリ名と同じ.exeファイルが生成されます。LinuxやmacOS向けであれば、拡張子のない実行ファイルが生成されることがあります。

生成されたファイルを実行して、正しく動くか確認します。

Windowsの場合は次のように実行します。

PowerShell
.\MyApp.exe

LinuxやmacOSの場合は、必要に応じて実行権限を付与します。

Bash
chmod +x ./MyApp
./MyApp

Native AOTでは、発行されたアプリに実行に必要なコードが含まれます。公式ドキュメントでも、publishディレクトリ内のアプリには実行に必要なコードが含まれ、縮小されたCoreCLRランタイムも含まれると説明されています。Microsoft Learn

4-5. よくあるエラーと対処法

Native AOTでよくあるエラーには、次のようなものがあります。

まず、ネイティブビルドツールが不足しているケースです。WindowsならC++ビルドツール、Linuxならclangやzlib関連パッケージ、macOSならXcode Command Line Toolsを確認しましょう。

次に、リフレクションや動的コード生成に関する警告です。Native AOTでは、実行時に型情報を動的に調べたり、コードを生成したりする処理が制限されます。警告が出た場合は、該当箇所を静的に扱える形に変更する必要があります。

また、使用しているNuGetパッケージがNative AOTに対応していない場合もあります。特に、ORM、DIコンテナ、JSONシリアライザー、プラグイン機構を持つライブラリでは注意が必要です。

5. C#をネイティブコンパイルするメリット

5-1. 起動時間を短縮しやすい

Native AOTの大きなメリットは、起動時間を短縮しやすいことです。

通常のC#アプリでは、起動時や初回実行時にJITコンパイルが発生する場合があります。Native AOTでは、発行時にネイティブコードへ変換されているため、実行時のJITコンパイルが不要になります。

そのため、短時間で起動して処理を終えるCLIツール、バッチ処理、サーバーレス関数などでは特に効果が出やすいです。

5-2. メモリ使用量を抑えやすい

Native AOTでは、実行時にJITコンパイラを使わないため、JIT関連のメモリ使用を抑えやすくなります。また、トリミングによって未使用コードを削減できる場合があります。

Microsoftの公式ドキュメントでも、Native AOTアプリは従来の.NETアプリに比べて起動が速く、メモリ使用量を小さくできると説明されています。Microsoft Learn

ただし、必ずすべてのケースでメモリ使用量が劇的に減るわけではありません。アプリの構成、使用ライブラリ、処理内容によって結果は異なります。

5-3. .NETランタイムなしで配布しやすい

Native AOTで発行したアプリは、配布先に.NETランタイムがインストールされていない環境でも実行しやすくなります。

これは、社内ツールや配布用CLIツールでは大きなメリットです。利用者に「先に.NETランタイムをインストールしてください」と案内しなくてもよい場面が増えるからです。

ただし、対象OSやCPUアーキテクチャごとにビルドする必要があります。Windows x64向けに発行した実行ファイルは、Linux x64ではそのまま動きません。

5-4. コンテナやCLIツールと相性がよい

Native AOTは、コンテナ環境やCLIツールと相性がよいです。

コンテナでは、イメージサイズ、起動速度、メモリ使用量が重要になります。Native AOTを使うことで、軽量なコンテナイメージを作りやすくなる場合があります。

CLIツールでは、ユーザーがコマンドを実行してすぐに結果が返ることが重要です。Native AOTによって起動時間を短縮できれば、体感速度の向上につながります。

5-5. 配布ファイルをシンプルにできる

Native AOTでは、配布ファイルをシンプルにしやすい点もメリットです。

通常の.NETアプリをフレームワーク依存で配布する場合、配布先に.NETランタイムが必要です。自己完結配置にするとランタイム込みで配布できますが、ファイル数が多くなる場合があります。

Native AOTでは、単一のネイティブ実行ファイルとして扱いやすくなります。配布、コピー、実行の流れがシンプルになるため、小規模なツールでは運用しやすくなります。

6. C#をネイティブコンパイルするデメリット・注意点

6-1. すべてのC#機能やライブラリが対応しているわけではない

Native AOTは便利ですが、すべてのC#機能や.NETライブラリが問題なく使えるわけではありません。

特に、実行時に型を探索する処理、動的にアセンブリを読み込む処理、実行時コード生成に依存するライブラリは注意が必要です。

公式ドキュメントでも、Native AOTには動的ロード、実行時コード生成、C++/CLI、組み込みCOM、トリミングや単一ファイル化に由来する制限などがあると説明されています。Microsoft Learn

6-2. リフレクションや動的コード生成に制限がある

Native AOTでは、リフレクションが完全に使えないわけではありません。しかし、通常のJIT実行と同じ感覚で使うと問題になることがあります。

たとえば、次のような処理は注意が必要です。

C#
var type = Type.GetType("MyApp.Models.User");
var instance = Activator.CreateInstance(type!);

このようなコードは、実行時に型名から型を探し、インスタンスを作成します。Native AOTでは、発行時に必要な型やメンバーを解析するため、実行時にしかわからない型情報はトリミング対象になったり、警告の原因になったりします。

また、System.Reflection.Emitのような実行時コード生成はNative AOTでは使えません。Microsoft Learn

6-3. 実行ファイルサイズが大きくなる場合がある

Native AOTは、必ずファイルサイズが小さくなるとは限りません。

フレームワーク依存の通常配布と比べると、Native AOTアプリには実行に必要なランタイムライブラリが含まれるため、ファイルサイズが大きくなる場合があります。公式ドキュメントでも、Native AOTアプリは自己完結アプリと同様に必要なランタイムライブラリを含むため、フレームワーク依存アプリに比べてサイズが増えることがあると説明されています。Microsoft Learn

一方で、通常の自己完結配置よりは小さくなるケースもあります。どちらが有利かは、アプリの規模や依存ライブラリによって変わります。

6-4. ビルド時間が長くなることがある

Native AOTでは、発行時にネイティブコードへのコンパイルを行うため、通常のビルドより時間がかかることがあります。

開発中に何度もNative AOT発行を行うと、ビルド時間が気になる場合があります。そのため、普段の開発では通常のdotnet runやJIT実行を使い、リリース前や互換性確認のタイミングでNative AOT発行を行う運用が現実的です。

6-5. プラットフォームごとにビルドが必要になる

Native AOTでは、対象プラットフォームごとにビルドが必要です。

たとえば、次のように分けて発行します。

Bash
dotnet publish -c Release -r win-x64
dotnet publish -c Release -r linux-x64
dotnet publish -c Release -r osx-arm64

Windows向けに作った実行ファイルをLinuxで実行することはできません。配布対象が複数ある場合は、CI/CDでプラットフォームごとのビルドを自動化するとよいでしょう。

6-6. デバッグやトラブルシューティングが難しくなる場合がある

Native AOTでは、通常の.NETアプリよりデバッグやトラブルシューティングが難しくなることがあります。

発行後の実行ファイルでは、JIT実行時とは異なる制約が表面化する場合があります。また、トリミングによって未使用と判断されたコードが削除され、実行時に想定外のエラーになることもあります。

Native AOT発行では、Linuxなら.dbg、Windowsなら.pdb、macOSなら.dSYMフォルダーといったデバッグ情報が生成されます。これらはデバッガーやクラッシュダンプ解析で必要になるため、本番運用では適切に保管しておくことが重要です。Microsoft Learn

7. Native AOTが向いているケース・向いていないケース

7-1. 向いているケース:CLIツール・小規模API・サーバーレス

Native AOTが向いている代表的なケースは、CLIツール、小規模API、サーバーレスです。

CLIツールでは、起動速度がユーザー体験に直結します。コマンドを実行するたびに待ち時間が発生すると使いづらくなります。Native AOTによって起動が速くなれば、ツールとしての使い勝手が向上します。

小規模APIでは、シンプルな構成で高速起動と低メモリを狙えます。特にMinimal APIのような軽量なASP.NET Coreアプリは、Native AOTと相性がよい場合があります。.NET 8以降では、ASP.NET CoreのMinimal API、gRPC、Worker ServiceなどでNative AOTのサポートが説明されています。Microsoft Learn

サーバーレスでは、コールドスタートが課題になることがあります。Native AOTによって起動時間を短縮できれば、コールドスタート対策として役立つ可能性があります。

7-2. 向いているケース:高速起動が重要なアプリ

高速起動が重要なアプリもNative AOTに向いています。

たとえば、次のようなアプリです。

・短時間で終了するバッチ処理
・社内向けの単体実行ツール
・監視用エージェント
・軽量なマイクロサービス
・コンテナで頻繁に起動されるサービス

これらは、アプリの実行時間に対して起動時間の割合が大きくなることがあります。Native AOTで起動処理を軽くできれば、全体の処理時間短縮につながります。

7-3. 向いていないケース:リフレクションを多用するアプリ

リフレクションを多用するアプリは、Native AOTに向いていない場合があります。

たとえば、実行時にアセンブリをスキャンして型を探す処理、属性を読み取って自動登録する処理、文字列から型を生成する処理などは注意が必要です。

依存性注入、シリアライズ、ORM、マッピングライブラリなどでは、内部的にリフレクションを使っていることがあります。これらを使う場合は、Native AOT対応状況を確認しましょう。

7-4. 向いていないケース:動的プラグインや高度なORMを使うアプリ

実行時にプラグインDLLを読み込むアプリは、Native AOTと相性が悪いです。Native AOTでは、Assembly.LoadFileのような動的ロードが制限されます。Microsoft Learn

また、高度なORMを使っているアプリも注意が必要です。ORMは、エンティティ型の解析、プロキシ生成、式ツリー、リフレクションなどを使うことがあります。Native AOTに対応しているORMや機能範囲を選ぶ必要があります。

動的な拡張性を重視するアプリでは、通常のJIT実行のほうが適していることも多いです。

7-5. Webアプリや業務システムで採用する際の判断基準

Webアプリや業務システムでNative AOTを採用する場合は、慎重に判断する必要があります。

判断基準は次のとおりです。

・起動速度が本当に重要か
・メモリ使用量の削減が必要か
・使用しているライブラリがNative AOTに対応しているか
・リフレクションや動的ロードを多用していないか
・プラットフォームごとのビルドを運用できるか
・通常のJIT実行で十分ではないか

大規模な業務システムでは、起動時間よりも開発効率、保守性、ライブラリ互換性が重視されることがあります。その場合、無理にNative AOTを採用するより、通常のJIT実行や自己完結配置で十分なこともあります。

8. C#のネイティブコンパイルでよく使う設定

8-1. PublishAotの基本設定

Native AOTを有効にする基本設定は、PublishAotです。

XML
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>

この設定を追加すると、発行時にNative AOTコンパイルが有効になります。また、ビルド中や編集時にAOT互換性に関する分析が有効になります。ASP.NET CoreのNative AOTドキュメントでも、PublishAotによって発行時のNative AOTコンパイルと動的コード使用状況の分析が有効になると説明されています。Microsoft Learn

8-2. RuntimeIdentifierの指定方法

RuntimeIdentifierは、対象となる実行環境を指定するための値です。コマンドラインでは-rオプションで指定します。

Bash
dotnet publish -c Release -r win-x64

プロジェクトファイルに書く場合は、次のように指定できます。

XML
<PropertyGroup>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>

複数の環境に配布する場合は、CI/CDでRIDごとに発行するのが一般的です。

代表的なRIDには次のようなものがあります。

win-x64
win-arm64
linux-x64
linux-arm64
osx-x64
osx-arm64

8-3. SelfContainedとの関係

Native AOTは自己完結型の配布モデルを使います。つまり、アプリの実行に必要なものを成果物に含める考え方です。

通常の.NET発行では、SelfContainedtrueにして.NETランタイム込みで配布することがあります。

XML
<PropertyGroup>
<SelfContained>true</SelfContained>
</PropertyGroup>

Native AOTの場合、ネイティブコード化されたアプリに実行に必要なコードが含まれるため、.NETランタイムが別途インストールされていない環境でも実行しやすくなります。

ただし、「SelfContainedをtrueにしただけ」ではNative AOTにはなりません。Native AOTを使うにはPublishAotが必要です。

8-4. トリミング設定と警告の見方

Native AOTではトリミングが重要です。トリミングとは、使われていないと判断されたコードを削減する仕組みです。

一見便利ですが、リフレクションで動的に使われる型やメンバーは、静的解析では使われていないと判断されることがあります。その結果、必要なコードが削除され、実行時エラーにつながる場合があります。

Native AOT発行時に警告が出た場合は、無視しないことが重要です。ASP.NET Coreの公式ドキュメントでも、Native AOTの分析はアプリケーションコードと依存ライブラリ全体を対象とし、警告を確認して修正することが推奨されています。Microsoft Learn

8-5. Releaseビルドで確認すべきポイント

Native AOTの確認は、基本的にReleaseビルドで行います。

Bash
dotnet publish -c Release -r win-x64

Releaseビルドでは、最適化やトリミングが有効になり、本番に近い形で成果物を確認できます。

確認すべきポイントは次のとおりです。

・起動時間は短くなっているか
・メモリ使用量は改善しているか
・実行ファイルサイズは許容範囲か
・警告が出ていないか
・JSONシリアライズやDIが正しく動くか
・対象OSで実際に実行できるか

Debugビルドで動いたとしても、Release発行後に問題が出ることがあります。必ず発行成果物を実行して確認しましょう。

9. C#ネイティブコンパイル時によくあるトラブル

9-1. ライブラリがNative AOTに対応していない

よくあるトラブルのひとつが、使用しているライブラリがNative AOTに対応していないケースです。

Native AOTでは、アプリ本体だけでなく依存ライブラリも含めて解析されます。そのため、NuGetパッケージ内部でリフレクションや動的コード生成を使っていると、警告や実行時エラーにつながる場合があります。

対策としては、次の方法があります。

・Native AOT対応を明記しているライブラリを選ぶ
・ライブラリの最新版に更新する
・リフレクション依存の少ない機能に切り替える
・代替ライブラリを検討する
・小さなサンプルで先に検証する

ライブラリ開発者向けには、IsAotCompatibleというプロパティでAOT互換性を示す仕組みも用意されています。Microsoft Learn

9-2. リフレクション関連の警告が出る

Native AOT発行時に、リフレクション関連の警告が出ることがあります。

たとえば、次のようなコードは警告の原因になりやすいです。

C#
var type = Type.GetType(typeName);
var method = type?.GetMethod(methodName);
method?.Invoke(null, null);

このコードでは、実行時に文字列から型やメソッドを探しています。AOTコンパイラは、発行時点で必要な型やメソッドを判断する必要があるため、こうした動的な処理は苦手です。

対策としては、型を明示的に参照する、ソースジェネレーターを使う、必要な型情報を事前に定義するなどの方法があります。

9-3. 実行ファイルが想定より大きい

Native AOTを使ったのに、実行ファイルが想定より大きいと感じることがあります。

原因としては、次のようなものがあります。

・必要なランタイムライブラリが含まれている
・ジェネリック型の特殊化でコード量が増えている
・デバッグ情報が含まれている
・依存ライブラリが多い
・トリミングしにくいコードがある

公式ドキュメントでも、ジェネリック型の組み合わせによってコードが事前生成され、ディスクサイズに大きな影響を与える可能性があると説明されています。Microsoft Learn

サイズを抑えたい場合は、依存関係を減らす、不要な機能を外す、デバッグ情報の扱いを確認する、コンテナイメージ全体のサイズも含めて評価することが重要です。

9-4. Windows以外でビルドに失敗する

LinuxやmacOSでNative AOTのビルドに失敗する場合、多くはネイティブツールチェーン不足が原因です。

Ubuntuでは、まず次を確認しましょう。

Bash
sudo apt-get install clang zlib1g-dev

Alpine Linuxでは、次のようなパッケージが必要です。

Bash
sudo apk add clang build-base zlib-dev

macOSでは、Xcode Command Line Toolsを確認します。

Bash
xcode-select --install

また、対象RIDが正しいかも確認しましょう。Linux x64向けなのにwin-x64を指定している、Arm64環境なのにx64向けの成果物を実行している、といったミスもあります。

9-5. JSONシリアライズでエラーが発生する

Native AOTで特に注意したいのが、JSONシリアライズです。

System.Text.Jsonは通常、リフレクションを使ってシリアライズやデシリアライズを行うことがあります。しかし、Native AOTではリフレクションに制限があるため、実行時に問題が起きる場合があります。Microsoftの公式ドキュメントでも、System.Text.Jsonは既定でリフレクションを使うため、Native AOTアプリで問題になることがあり、ソース生成の利用が説明されています。Microsoft Learn

対策としては、JSONソースジェネレーターを使います。

例として、次のようなモデルがあるとします。

C#
public class User
{
public string Name { get; set; } = "";
public int Age { get; set; }
}

ソース生成用のコンテキストを定義します。

C#
using System.Text.Json.Serialization;

[JsonSerializable(typeof(User))]
internal partial class AppJsonSerializerContext : JsonSerializerContext
{
}

使用時は、次のようにコンテキストを指定します。

C#
var user = new User { Name = "Taro", Age = 30 };

var json = JsonSerializer.Serialize(
user,
AppJsonSerializerContext.Default.User);

このように、実行時のリフレクションに頼らず、ビルド時に必要なシリアライズコードを生成することで、Native AOTと相性のよい実装にできます。

10. C#をネイティブコンパイルする前に確認すべきポイント

10-1. アプリの目的にNative AOTが合っているか

まず確認すべきなのは、アプリの目的にNative AOTが合っているかです。

Native AOTは、起動時間、メモリ使用量、配布のしやすさを重視する場合に向いています。一方で、開発の柔軟性、ライブラリ互換性、動的な拡張性を重視する場合は、通常のJIT実行のほうが適していることもあります。

「ネイティブコンパイルできるから使う」のではなく、「このアプリではNative AOTのメリットがデメリットを上回るか」を考えることが重要です。

10-2. 使用ライブラリが対応しているか

Native AOTを採用する前に、使用しているライブラリの対応状況を確認しましょう。

特に注意すべきライブラリは次のとおりです。

・ORM
・DIコンテナ
・JSONライブラリ
・マッピングライブラリ
・プラグイン機構を持つライブラリ
・テンプレートエンジン
・動的プロキシ生成を使うライブラリ

これらは内部でリフレクションや動的コード生成を使っている場合があります。公式ドキュメントでも、Native AOTの発行プロセスはプロジェクトと依存関係全体を解析し、実行時に発生しうる制限について警告を出すとされています。Microsoft Learn

10-3. 起動速度・メモリ・配布方法のどれを重視するか

Native AOTを検討する際は、何を重視するのかを明確にしましょう。

起動速度を重視するなら、CLIツールやサーバーレスで効果を測定します。

メモリ使用量を重視するなら、通常のJIT実行、自己完結配置、Native AOTのそれぞれで実測します。

配布方法を重視するなら、実行ファイルの数、ランタイムインストールの有無、対象OSの数を確認します。

Native AOTはメリットが多い一方で、制約もあります。目的がはっきりしていれば、採用すべきか判断しやすくなります。

10-4. 通常のJIT実行との比較検証を行う

Native AOTを採用する前に、通常のJIT実行と比較しましょう。

比較すべき項目は次のとおりです。

・起動時間
・初回リクエストの応答時間
・メモリ使用量
・実行ファイルサイズ
・ビルド時間
・発行時の警告数
・本番環境での運用しやすさ

Native AOTは、すべてのアプリで必ず最適解になるわけではありません。実測した結果、通常のJIT実行で十分な場合もあります。

特に、長時間起動し続けるWebアプリでは、起動時間のメリットが相対的に小さくなることがあります。その場合は、メモリ使用量やコンテナサイズなども含めて判断しましょう。

10-5. 小さなプロジェクトから試すのがおすすめ

初めてC#のネイティブコンパイルを試すなら、小さなプロジェクトから始めるのがおすすめです。

たとえば、次のような簡単なコンソールアプリを作ります。

Bash
dotnet new console -n NativeAotSample
cd NativeAotSample

.csprojPublishAotを追加します。

XML
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>

Windows x64向けに発行します。

Bash
dotnet publish -c Release -r win-x64

まずは依存ライブラリの少ない状態で、Native AOTの発行、実行ファイルの確認、起動速度、ファイルサイズを体験しましょう。その後、JSON、DI、HTTP通信、データベースアクセスなどを少しずつ追加していくと、どこで問題が起きるか把握しやすくなります。

まとめ

C#のネイティブコンパイルとは、C#アプリケーションを特定のOSやCPU向けのネイティブコードへ変換することです。現在の.NETでは、Native AOTを使うことで、C#アプリを発行時にネイティブコード化し、JITコンパイルに依存しない実行ファイルとして配布できます。

Native AOTの主なメリットは、起動時間を短縮しやすいこと、メモリ使用量を抑えやすいこと、.NETランタイムなしで配布しやすいことです。CLIツール、小規模API、サーバーレス、コンテナ環境などでは特に効果を期待できます。

一方で、リフレクション、動的コード生成、動的ロード、対応していないライブラリには注意が必要です。実行ファイルサイズが大きくなる場合や、ビルド時間が長くなる場合もあります。また、Windows、Linux、macOSなど、対象プラットフォームごとにビルドが必要です。

Native AOTを使う基本手順は、プロジェクトファイルに<PublishAot>true</PublishAot>を追加し、dotnet publish -c Release -r <RID>で発行するだけです。しかし、実際のプロジェクトでは、依存ライブラリ、JSONシリアライズ、トリミング警告、デバッグ情報などを丁寧に確認する必要があります。

C#をネイティブコンパイルしたい場合は、まず小さなプロジェクトでNative AOTを試し、通常のJIT実行との違いを実測することが大切です。そのうえで、アプリの目的に合っていると判断できれば、Native AOTはC#アプリの配布やパフォーマンス改善に役立つ強力な選択肢になります。