このページには自動翻訳されたテキストを含めることができます。
.NET で Native AOT アプリケーションを開発する方法
.NET 8 では、事前コンパイル (Native AOT とも呼ばれる) が包括的にサポートされています。このようなアプリケーションは通常、マネージド ソリューションよりも高速で、メモリ消費量も少なくなります。
.NET AOT 公開では、トリミングされた自己完結型のアプリケーションが生成されます。このようなアプリケーションは、次のようになります。
- .NET ランタイムがインストールされていないマシンで実行できます。
- Windows x64 などの特定のランタイム環境を対象としています。
- 未使用のコードが含まれていません。
PDF 処理用の Docotic.Pdf ライブラリを開発しています。AOT 互換性は、2024 年に最も多くリクエストされた機能の 1 つです。この記事では、AOT 互換性を実現するための取り組みについて説明します。.NET プロジェクトで AOT の問題を見つけて修正する方法を学びます。
.NET AOT 公開の基本
Native AOT アプリケーションを公開するには、プロジェクト ファイルに次のプロパティを追加します:
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
いくつかの 前提条件
をインストールする必要がある場合もあります。その後、Visual Studio からアプリを公開できます。または、dotnet publish
コマンドを使用して、コマンド ラインからプロジェクトを公開することもできます。
dotnet publish -r linux-x64 -c Release
.NET ライブラリの AOT 互換性
Native AOT 公開は、.NET 7 および .NET 8 で利用できます。.NET 7 では、コンソール アプリケーションのみを公開できます。.NET 8 では、さらに ASP.NET Core アプリケーションを公開できます。
しかし、Docotic.Pdf コア ライブラリは .NET Standard 2.0 をターゲットにしています。これを AOT 対応にすることはできますか? もちろんです。
.NET Standard、.NET 5、または .NET 6 を対象とする .NET ライブラリで、AOT 互換性の問題を見つけて修正することはできます。Native AOT アプリケーションは、そのライブラリに依存できます。
AOT の問題を見つける
AOT 互換性に関する最初のサポート リクエストは次のようになります:
WebAssembly にコンパイルされた Docotic.Pdf の使用を検討しています。これを AOT にコンパイルすると、トリム警告が生成されます:
警告 IL2104: アセンブリ 'BitMiracle.Docotic.Pdf' でトリム警告が生成されました。詳細については、https://aka.ms/dotnet-illink/libraries を参照してください
あまり有益ではありません。問題を再現して、これらのトリム警告について詳細を取得しましょう。
公開警告を取得する
テスト アプリケーションを公開することは、.NET AOT の問題を見つける最も柔軟な方法です。次の操作が必要です:
PublishAot = true
で .NET 8 コンソール プロジェクトを作成します- プロジェクトへの参照を追加し、その型のいくつかを使用します
Docotic.Pdf に使用する csproj 構成は次のとおりです:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<TrimmerSingleWarn>false</TrimmerSingleWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Docotic\Docotic.csproj" />
</ItemGroup>
</Project>
TrimmerSingleWarn = false
を使用すると、警告に関する詳細情報を取得できます。そうでない場合は、「アセンブリ
'X' がトリム警告を生成しました」というメッセージのみが表示されます。
そして、Program.cs
の C# コードは次のとおりです:
using BitMiracle.Docotic.Pdf;
using var pdf = new PdfDocument();
チェック対象のアセンブリを強制的にロードするコードを追加します。公開警告を取得するために、プロジェクトのすべての機能をカバーする必要はありません。単一の型を使用すれば十分です。ただし、ランタイム エラーをキャッチ するには、さらにコードを記述する必要があります。
このプロジェクトの dotnet publish -r win-x64 -c Release
コマンドは、トリムおよび AOT
警告を返しました。リストの短縮版は次のとおりです:
AOT 分析警告 IL3050: Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): 'RequiresUnreferencedCodeAttribute' を持つメンバー 'System.Linq.Expressions.Expression.Property(Expression,String)' を使用すると、アプリケーション コードをトリムするときに機能が壊れる可能性があります。
トリム分析警告 IL2026: LogProviders.Log4NetLogProvider.Log4NetLogger.GetCreateLoggingEvent(ParameterExpression,UnaryExpression,ParameterExpression,UnaryExpression,Type): 'RequiresUnreferencedCodeAttribute' を持つメンバー 'System.Linq.Expressions.Expression.Property(Expression,String)' を使用すると、アプリケーション コードをトリミングするときに機能が壊れる可能性があります。
トリム分析警告 IL2057: LogProviders.LogProviderBase.FindType(String,String[]): メソッド 'System.Type.GetType(String)' のパラメータ 'typeName' に認識されない値が渡されました。
トリム分析警告 IL2070: LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String): 'this' 引数は、'System.Type.GetProperty(String)' の呼び出しで 'DynamicallyAccessedMemberTypes.PublicProperties' を満たしていません。メソッド 'LogProviders.NLogLogProvider.NLogLogger.GetIsEnabledDelegate(Type,String)' のパラメータ 'loggerType' には、一致する注釈がありません。ソース値は、割り当て先のターゲット ロケーションで宣言されている要件と少なくとも同じ要件を宣言する必要があります。
トリム分析警告 IL2075: LogProviders.Log4NetLogProvider.GetOpenNdcMethod(): 'this' 引数は、'System.Type.GetProperty(String)' の呼び出しで 'DynamicallyAccessedMemberTypes.PublicProperties' を満たしていません。メソッド 'LogProviders.LogProviderBase.FindType(String,String)' の戻り値には、一致する注釈がありません。ソース値は、割り当て先のターゲットの場所で宣言されている要件と少なくとも同じ要件を宣言する必要があります。
根本的な原因が見つかりました。問題を修正する前に、AOT 互換性の問題を見つけるための代替オプションを確認しましょう。
AOT 互換性アナライザーを使用する
Microsoft は、.NET 7 または .NET 8 プロジェクトの AOT の問題を見つけるための Roslyn アナライザーを提供しています。コードを書くときに AOT の問題をハイライト表示できます。ただし、テスト アプリケーションを公開すると、さらに多くの問題が検出されます。
静的分析を使用するには、次のプロパティを .csproj ファイルに追加します:
<PropertyGroup>
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>
.NET Standard 2.0 プロジェクトでこのような分析を使用するには、.NET 7 または .NET 8 フレームワークをターゲットにする必要があります。プロジェクト構成は次のようになります。
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<IsAotCompatible Condition="'$(TargetFramework)'=='net8.0'">true</IsAotCompatible>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
</PropertyGroup>
上記の構成を大規模なコア プロジェクトに適用したところ、1036 件のビルド エラーが発生しました。
これらはすべて AOT の互換性に関係するものだったのでしょうか? プロジェクトを IsAotCompatible
プロパティなしで再構築しました。それでも 1036 個のエラーがありました。
すべてのエラーは、.NET Standard 2.0 と比較した .NET 8 の改善に関連しています。私たちの場合、ほとんどの問題は、null
許容参照型のサポートの改善に関するものでした。また、新しい ArgumentOutOfRangeException.ThrowIfLessThan
および ArgumentNullException.ThrowIfNull
ヘルパー メソッドに関する CA1500 および CA1512 エラーも多数ありました。
すべてのエラーを修正し、IsAotCompatible
プロパティをオンにしました。プロジェクトのビルドは AOT
エラーなしで終了しました。静的分析では、公開プロセス中に見つかった警告は 1 つもキャッチされませんでした 🙁
確認のため、コード ベースに次のテスト コードを追加しました:
Type t = typeof(int);
t = typeof(List<>).MakeGenericType(t);
Console.WriteLine(Activator.CreateInstance(t));
string s = Console.ReadLine() ?? string.Empty;
Type? type = Type.GetType(s);
if (type != null)
{
foreach (var m in type.GetMethods())
Console.WriteLine(m.Name);
}
この C# コードは、.NET AOT およびトリミングと 100% 互換性がありません。また、静的分析により、次のエラーが特定されました:
エラー IL3050: 'RequiresDynamicCodeAttribute' を持つメンバー 'System.Type.MakeGenericType(params Type[])' を使用すると、AOT コンパイル時に機能が壊れる可能性があります。このインスタンス化の ネイティブ コードは、実行時に使用できない可能性があります。
エラー IL2057: メソッド 'System.Type.GetType(String)' のパラメータ 'typeName' に認識されない値が渡されました。ターゲット タイプの可用性を保証することはできません。
つまり、Roslyn アナライザーは機能し、いくつかの問題を見つけることができます。私たちのケースでは、何も見つかりませんでした。
ランタイム エラーをキャッチする
ライブラリを Native AOT と互換性のあるものにする方法 の記事には、次の原則が記載されています。
アプリケーションが AOT 用に公開されるときに警告がない場合、AOT 後も AOT なしの場合と同じように動作します。
実際には、AOT 後のバージョンは、まだ異なる動作をする可能性があります。すべての AOT およびトリムの警告を修正しました。すべての自動テストに合格しました。テスト アプリケーションは PDF ドキュメントを適切に処理しました。そして、同じアプリケーションの AOT バージョンは、間違ったドキュメントを生成しました。
AOT の公開により、必要なコードが誤って削除されました。最適化により必要なコードが削除される 問題が発生しました。.NET チームはすぐに確認して修正しました。また、修正が利用可能になるまで、C# コードに回避策を追加しました。
この話の教訓は、公開と静的分析の警告だけでは十分ではない可能性があるということです。テスト アプリケーションでプロジェクトの主要な機能を使用します。そして、展開後にテスト アプリケーションを実行します。
Docotic.Pdf テスト アプリケーションは、PDF 圧縮、テキスト抽出、およびその他の主要な機能を使用します。次のように使用します:
AotCompatibility.exe compress "C:\compressed.pdf" "C:\input.pdf" "optional-password"
公開されたアプリケーションを手動でテストするのは不便です。プロセスを自動化する価値があります。
AOT の問題を自動的に検出
Docotic.Pdf の開発は 自動テスト に基づいています。すべての機能やバグに対してテストがあります。.NET AOT 互換性も例外ではありません。
Native AOT アプリケーションのテストを自動化する方法は 2 つあります。
AOT アプリケーションの外部にテストを配置する
ここでは、通常の NUnit/xUnit/MSTest プロジェクトを使用します。このプロジェクトをビルドする前に、AOT テスト アプリケーションを公開します。テストでは、Process.Start メソッドを使用して公開されたアプリを実行します。
主な利点は、テスト インフラストラクチャを AOT アプリから分離できることです。ユニット テスト フレームワークの全機能を使用できます。テスト フレームワークの AOT 互換性の問題は問題になりません。既存のテスト ケースを再利用することも簡単になります。
テスト プロジェクトのサンプル構成 は GitHub で入手できます。Docotic.Tests
プロジェクトには、マネージド バージョンと AOT バージョンの両方の自動テストが含まれています。AotCompatibility
は、AOT 公開用のテスト アプリケーションです。
AotCompatibility.csproj
は、Release
構成のビルド後イベントでテスト アプリケーションを公開します。Visual
Studio で「公開」ウィザードを使用する場合、'$(PublishProtocol)'==''
条件によりこの手順が無効になります。
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Release' And '$(PublishProtocol)'==''">
<Exec Command=""$(SolutionDir)Scripts\publish.bat"" />
</Target>
publish.bat
スクリプトはシンプルです:
@echo off
SET AOT_TEST_DIR=%~dp0\..\AotCompatibility
dotnet publish "%TRIM_TEST_DIR%\AotCompatibility.csproj" --no-build /warnaserror /p:PublishProfile="%AOT_TEST_DIR%\Properties\PublishProfiles\win-x64-installer.pubxml"
ここで --no-build
引数を使用していることに注意してください。プロジェクトは、ビルド後のイベントの前にすでにビルドされています。再度ビルドする必要はありません。
NativeAot.cs
のテストは、公開された AotCompatibility.exe
を異なる引数で実行します。Docotic.Tests
プロジェクトと AotCompatibility
プロジェクトは、同じテスト済みコードを共有します。これにより、コードを重複させることなく、管理対象バージョンと AOT バージョンをチェックできます。
AOT アプリケーション内にテストを配置する
別のアプローチとしては、AOT テスト アプリケーション内でテストを記述する方法があります。2024 年 7 月には、MS テスト早期プレビューのみを使用できます。詳細については、Native AOT アプリケーションのテスト の記事をお読みください。
主な問題は、テスト フレームワークに AOT の問題があると、テストが信頼できなくなることです。また、テスト フレームワークの機能セットは限られています。そのため、テスト インフラストラクチャを AOT アプリケーションの外部に配置することを推奨します。
AOT およびトリム警告を修正
公開警告を抑制しないでください。通常、抑制は誤った解決策です。AOT を抑制したり、警告をトリムしたりすると、プロジェクトは AOT と互換性があると言えます。しかし、警告の根本的な原因は依然として存在します。抑制された警告は、最終的にアプリケーションの実行時障害につながる可能性があります。
各警告に関連するコードを削除するか書き直してみてください。サードパーティ コンポーネントを更新すると役立つ場合もあります。
Docotic.Pdf の AOT およびトリム警告をどのように修正したかを確認しましょう。公開警告を取得する セクションですでにいくつか確認しました。
LibLog からのトリム警告
ログ記録には LibLog ライブラリを使用しました。LibLog は、一般的なログ記録フレームワークを検出して使用するためにリフレクションに依存しています。設計上、AOT とは互換性がありません。
現在、.NET でのログ記録の標準は Microsoft.Extensions.Logging
パッケージです。LibLog の開発は凍結されています。
そのため、コードから LibLog を完全に削除しました。代わりに、Docotic.Pdf.Logging
アドオンをリリースしました。これは Microsoft.Extensions.Logging.Abstractions
インターフェイスに依存しています。これにより、コア ライブラリのサイズが削減され、関連するトリム警告がすべて修正されました。
Docotic.Layout アドオンのデバッグ コード
Docotic.Layout アドオン では、内部デバッグにリフレクションを使用しています。これにより、警告 IL2075 が発生しました。
対応するコードはコード ベースに存在しますが、クライアントはパブリック API 経由で使用できません。解決策は、Release
構成からコードを除外することです。現在は Debug
構成でのみ使用されています。
BouncyCastle からの AOT 警告 IL3050
BouncyCastle ライブラリは、PDF ドキュメントに署名するのに役立ちます。次の警告は、BouncyCastle コードから出力されます:
Org.BouncyCastle.Utilities.Enums.GetEnumValues(Type): 'RequiresDynamicCodeAttribute' を持つメンバー 'System.Enum.GetValues(Type)' を使用すると、AOT コンパイル時に機能が壊れる可能性があります。実行時に列挙型の配列を作成できない可能性があります。代わりに、GetValues<TEnum> オーバーロードまたは GetValuesAsUnderlyingType メソッドを使用してください。
BouncyCastle は Enum.GetValues(Type) メソッドを使用しますが、これは AOT と互換性がありません。
最も簡単な解決策は、代わりに Enum.GetValues<T>()
オーバーロードを使用することです。最新の BouncyCastle
バージョンでは既に使用されています。残念ながら、このオーバーロードは .NET 5 以降でのみ使用できます。これは
.NET Standard 2.0 のオプションではありません。
BouncyCastle コードを詳しく分析したところ、BouncyCastle
は列挙定数の難読化を防ぐためにこれを使用していることが判明しました。代わりに、[System.Reflection.Obfuscation(Exclude = true)]
属性を使用して、対応する列挙の難読化を無効にしました。また、Enum.GetValues(Type)
の使用は不要になりました。
難読化されたビルドでの奇妙な問題
難読化を使用して本番ビルドを保護します。驚いたことに、難読化されたビルドの .NET AOT デプロイメントでは、次のような警告が返されました:
<不明>:0: エラー: シンボル '__GenericLookupFromType_BitMiracle_Docotic_Pdf___4<System___Canon__System___Canon__System___Canon__Int32>_TypeHandle___System___Canon' は既に定義されています
このようなエラーには、関連する IL30##
または IL2###
コードがありませんでした。関連するコードを識別する方法が不明でした。
エラーは難読化されたバージョンでのみ発生しました。難読化ツールのバグである可能性があります。難読化ツールを更新しましたが、エラーは依然として存在していました。
問題の範囲を絞り込む必要がありました。バイナリ検索に基づいて、次のプロセスを使用しました。
- 名前空間の半分の難読化を無効にします。エラーが発生するかどうかを確認します。エラーの原因となる名前空間が見つかるまで続けます。
- ステップ 1 の名前空間の型の半分の難読化を無効にします。エラーが発生するかどうかを確認します。エラーの原因となる型が見つかるまで続けます。
- ステップ 2 の型のメンバーの半分の難読化を無効にします。エラーが発生するかどうかを確認します。エラーの原因となるメンバーが見つかるまで続けます。
- ステップ 3 のメンバーのコードを確認します。異常な部分をコメント アウトし、エラーが発生するかどうかを確認します。
結局、ステップ 1 に戻りました 🙂 すべての 名前空間の難読化を無効にした後も、エラーは発生しました。
生成されたアセンブリを ILSpy で分析しました。コンパイラによって生成された予期しない型がいくつか見つかりました。ILSpy は、次のような C# コードからこれらの型の使用を示しました:
interface X
{
void f();
}
X obj = ..;
if (obj.f != null)
..
この奇妙なコードは、旧式の C コードを C# に移行
した際に発生しました。if (obj.f != null)
条件は完全に冗長です。このような条件を削除したところ、エラーはなくなりました。
GetCallingAssembly を使用しないでください
別の ランタイムの問題 が見つかりました。AOT の公開後に LicenseManager.AddLicenseData(string) メソッドの呼び出しが失敗し、次のエラーが発生しました:
Assembly.GetCallingAssembly() は System.PlatformNotSupportedException をスローします: このプラットフォームでは操作はサポートされていません。
.NET 9 の Assembly.GetCallingAssembly
メソッド Native AOT
用に実装する必要があります。修正が完了するまで、悪影響を最小限に抑えるためにコードをリファクタリングしました。現在は、呼び出しアセンブリ属性を
Application ライセンス の検証にのみ使用しています。その他のライセンス タイプは AOT と互換性があります。
特別な属性でコードをマークする
幸いなことに、コア Docotic.Pdf ライブラリとほとんどのアドオンで、すべての AOT およびトリム警告を修正できました。ただし、すべての AOT 非互換コードを書き換えることはできない可能性があります。
.NET は、このような状況に備えて特別な属性を提供します。既知の AOT の問題についてクライアントに通知するには、API に属性をマークします。API のユーザーは、マークされたメソッドを呼び出すときに警告を受け取ります。
AOT と互換性のない .NET コードをマークするための主な属性は次のとおりです。
RequiresDynamicCode
RequiresUnreferencedCode
DynamicallyAccessedMembers
これらの属性の詳細については、公式の AOT 警告の概要 および .NET ライブラリをトリミング用に準備する の記事をご覧ください。
結論
Native AOT の展開は、.NET の世界における大きな前進です。通常の C# コードを記述して、ネイティブ アプリケーションまたはライブラリを取得できます。このようなアプリケーションは通常、より高速で、.NET ランタイムをインストールしなくても実行できます。C、Rust、またはその他の非 .NET プログラミング言語から公開された DLL を使用することもできます。
AOT の互換性の問題を見つけるために、テスト用の .NET 8 アプリケーションを公開します。Roslyn アナライザーもありますが、検出される問題は少なくなります。
コア Docotic.Pdf ライブラリは、AOT およびトリミングと互換性を持つようになりました。次のアドオンも互換性があります:
- Layout アドオン
- Gdi アドオン
- Logging アドオン
HTML から PDF へのアドオンにはまだトリム警告が含まれています。これは、Native AOT ロードマップの次の目標です。
Native AOT や PDF 処理についてはお気軽に ご質問 ください。