一、はじめに
サイト管理者がAOTに触れてから約3ヶ月が経ちました。以前の記事「朗報:NET 9 X86 AOTのブレークスルー - 旧来のWin7及びXP環境をサポート」でも触れました。この間、Avaloniaを使用して開発したプロジェクトでもAOT公開テストを無事完了しました。しかし、このプロセスは順風満帆ではありませんでした。プロジェクトの機能が大半完了した段階でAOTテストを始めたため、多くの問題に遭遇し、いわゆる「多くの穴を踏む」状態となりました。将来の振り返りや読者の参考のために、この経験をまとめます。
.NET AOTは、.NETコードを事前にネイティブコードにコンパイルする技術です。その利点は多く、起動速度が速く、ランタイムリソースの消費を減らし、セキュリティも向上します。AOT公開後は、.NETランタイムなどの依存関係をインストールする必要がありません。.NET 8、9のAOT公開後、XPやWin7非SP1オペレーティングシステムでも実行可能です。これにより、アプリケーションの展開がより便利になり、より多くの旧来システム環境に対応でき、開発者にとってアプリケーションシーンが広がり、パフォーマンスが向上するとともに、システム互換性も向上し、.NETアプリケーションの開発と展開に柔軟性と広範性をもたらし、ユーザーにより良い体験を提供します。
二、経験談
(一)テスト戦略の重要性
プロジェクト作成当初から、新機能を追加したり新しい構文を使用したりするたびに、AOT公開テストを実施する習慣を身につけるべきです。そうしないと、問題が後期に蓄積され、解決が非常に困難になります。サイト管理者は前期にこの点を軽視したため、大きな代償を払いました。やむを得ない解決方法は、プロジェクトを再作成し、機能を一つずつ復元してAOTテストを行うことでした。一週間の残業によるAOTテストの後、各AOT公開プロセスは概ね以下のようになりました。
- 社内ネットワークでのAOT公開に2、3分かかり、その間は要求仕様書や技術記事を読むしかありませんでした…
- 公開完了後、実行しても効果がなく、ダブルクリックしてもインターフェースが表示されず、プロセスリストにも存在しないため、プログラムがクラッシュしたことを示しています。システムアプリケーションイベントログを確認すると、通常は例外警告情報が含まれています。
- ログ情報に基づいてコードを確認し、関連APIを修正。
- 再度AOT公開を行い、上記1~3の手順を繰り返す。
一週間の努力の末、プロジェクトのAOT後の機能テストが正常になり、ここで完了しました。
(二)AOTの注意点と解決方法
1. rd.xmlの追加
メインプロジェクトにXMLファイル(例:Roots.xml)を作成し、内容は以下の通りです。
<linker>
<assembly fullname="CodeWF.Toolbox.Desktop" preserve="All" />
</linker>
AOTをサポートする必要があるプロジェクトごとに、このXMLにassemblyノードを追加します。fullnameはアセンブリ名で、CodeWF.Toolbox.Desktopはサイト管理者のツールのメインプロジェクト名です。こちらでソースコードを確認できます。
メインプロジェクトにItemGroupノードを追加して、そのXMLファイルを関連付けます。
<ItemGroup>
<TrimmerRootDescriptor Include="Roots.xml" />
</ItemGroup>
2. Prismのサポート
サイト管理者はPrismフレームワークとDryIOCコンテナを使用しています。AOTをサポートするには、以下のNuGetパッケージを追加する必要があります。
<PackageReference Include="Prism.Avalonia" Version="8.1.97.11073" />
<PackageReference Include="Prism.DryIoc.Avalonia" Version="8.1.97.11073" />
rd.xmlには以下を追加します。
<assembly fullname="Prism" preserve="All" />
<assembly fullname="DryIoc" preserve="All" />
<assembly fullname="Prism.Avalonia" preserve="All" />
<assembly fullname="Prism.DryIoc.Avalonia" preserve="All" />
バージョン8.1.97.11073は最後のオープンソースバージョンで、9.X以降は有料バージョンです。
3. App.configの読み書き
.NET CoreでSystem.Configuration.ConfigurationManagerパッケージを使用してApp.configファイルを操作する場合、rd.xmlに以下を追加します。
<assembly fullname="System.Configuration.ConfigurationManager" preserve="All" />
Assembly.GetEntryAssembly().locationは失敗します。現在はConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None)を使用してアプリケーション設定を取得していますが、パス指定方法は後で調査します。
4. HttpClientの使用
rd.xmlに以下を追加します。
<assembly fullname="System.Net.Http" preserve="All" />
一般的にHttpClientを直接使用せず、Refitのようなサードパーティライブラリを試すとHTTPリクエストがより便利です。
<assembly fullname="Refit" preserve="All" />
5. Dapperのサポート
DapperのAOTサポートにはDapper.AOTパッケージのインストールが必要です。rd.xmlに以下を追加します。
<assembly fullname="Dapper" preserve="All" />
<assembly fullname="Dapper.AOT" preserve="All" />
データベース操作メソッドにはDapperAOT属性を追加する必要があります。例は以下の通りです。
[DapperAot]
public static bool EnsureTableIsCreated()
{
try
{
using var connection = new SqliteConnection(DBConst.DBConnectionString);
connection.Open();
const string sql = $@"
CREATE TABLE IF NOT EXISTS {nameof(JsonPrettifyEntity)}(
{nameof(JsonPrettifyEntity.IsSortKey)} Bool,
{nameof(JsonPrettifyEntity.IndentSize)} INTEGER
)";
using var command = new SqliteCommand(sql, connection);
return command.ExecuteNonQuery() > 0;
}
catch (Exception ex)
{
return false;
}
}
6. System.Text.Json
シリアル化
public static bool ToJson<T>(this T obj, JsonSerializerOptions? options, out string? json, out string? errorMsg)
{
if (obj == null)
{
json = default;
errorMsg = "Please provide object";
return false;
}
if (options == null)
{
options = new JsonSerializerOptions()
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
};
}
try
{
json = JsonSerializer.Serialize(obj, options);
errorMsg = default;
return true;
}
catch (Exception ex)
{
json = default;
errorMsg = ex.Message;
return false;
}
}
逆シリアル化
public static bool FromJson<T>(this string? json, JsonSerializerOptions? options, out T? obj, out string? errorMsg)
{
if (string.IsNullOrWhiteSpace(json))
{
obj = default;
errorMsg = "Please provide json string";
return false;
}
try
{
if (options == null)
{
options = new JsonSerializerOptions()
{
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
Converters = { new NullableDateTimeConverter() }
};
}
obj = JsonSerializer.Deserialize<T>(json!, options);
errorMsg = default;
return true;
}
catch (Exception ex)
{
obj = default;
errorMsg = ex.Message;
return false;
}
}
上記の設定で、単純なクラスのシリアル化と逆シリアル化は正常に動作します。クラスにint?やdouble?のようなnull許容型が存在する場合:
public class Project
{
public int? Id { get; set; }
public string Name { get; set; }
public int Record { get; set; }
[XmlArray(ElementName = "Members")]
[XmlArrayItem(typeof(Member))]
public List<Member> Members { get; set; }
}
その場合、JsonSerializerContextのサブクラスを作成することで解決できます。
[JsonSerializable(typeof(Project))]
[JsonSerializable(typeof(List<Member>))]
internal partial class ProjectJsonSerializerContext : JsonSerializerContext
{
}
注意:作成するだけでよく、明示的に呼び出す必要はありません。
従来の
JsonSerializer.Serialize(project)は、ランタイムのリフレクションに依存してProjectクラスのプロパティや属性などを解析しますが、AOTコンパイルでは明示的に参照されていないリフレクションメタデータがコンパイル時に除去され、シリアル化が失敗する(NotSupportedExceptionがスローされる、または一部のプロパティのシリアル化/逆シリアル化ができない)可能性があります。一方、
ProjectJsonSerializerContextはコンパイル時に静的メタデータを生成することで動作します。
7. リフレクションの問題
参考プロジェクト:CodeWF.NetWeaver
- 指定した型の
List<T>またはDictionary<T>インスタンスを作成:
public static object CreateInstance(Type type)
{
var itemTypes = type.GetGenericArguments();
if (typeof(IList).IsAssignableFrom(type))
{
var lstType = typeof(List<>);
var genericType = lstType.MakeGenericType(itemTypes.First());
return Activator.CreateInstance(genericType)!;
}
else
{
var dictType = typeof(Dictionary<,>);
var genericType = dictType.MakeGenericType(itemTypes.First(), itemTypes[1]);
return Activator.CreateInstance(genericType)!;
}
}
- リフレクションを使用して
List<T>とDictionary<T>のAddメソッドを呼び出して要素を追加する試みが失敗しました。以下は疑似コードです。
// List<T>
var addMethod = type.GetMethod("Add");
addMethod.Invoke(obj, new[]{ child })
// Dictionary<Key, Value>
var addMethod = type.GetMethod("Add");
addMethod.Invoke(obj, new[]{ key, value })
解決策:実装されたインターフェースに変換して呼び出します。
// List<T>
(obj as IList).Add(child);
// Dictionary<Key, Value>
(obj as IDictionary)[key] = value;
- 配列、
List<T>、Dictionary<key, value>の要素数を取得
上記のAddメソッドと同様に、リフレクションでLengthやCountプロパティを取得するとどちらも0を返します。value.Property("Length", 0)のように、カプセル化されたPropertyは非AOTでは正しく動作します。
public static T Property<T>(this object obj, string propertyName, T defaultValue = default)
{
if (obj == null) throw new ArgumentNullException(nameof(obj));
if (string.IsNullOrEmpty(propertyName)) throw new ArgumentNullException(nameof(propertyName));
var propertyInfo = obj.GetType().GetProperty(propertyName);
if (propertyInfo == null)
{
return defaultValue;
}
var value = propertyInfo.GetValue(obj);
try
{
return (T)Convert.ChangeType(value, typeof(T));
}
catch (InvalidCastException)
{
return defaultValue;
}
}
AOTで成功する方法:基本型や実装されたインターフェースにキャストして直接プロパティにアクセスします。
// 配列
var length = ((Array)value).Length;
// List<T>
if (value is IList list)
{
var count = list.Count;
}
// Dictionary<key, value>
if (value is IDictionary dictionary)
{
var count = dictionary.Count;
}
8. Windows 7のサポート
AOT後にWindows 7で実行できない場合は、YY-Thunksパッケージを追加してください。
<PackageReference Include="YY-Thunks" Version="1.1.4-Beta3" />
また、ターゲットフレームワークをnet9.0-windowsに指定します。
9. Winforms / XP互換性
8番の手順後も実行できない場合は、前回の記事「.NET 9 AOTのブレークスルー - 旧来のWin7とXP環境をサポート - 碼坊 (dotnet9.com)」を参照してVC-LTLパッケージを追加してください。ここでは詳しく説明しません。
10. その他
他にも注意すべき点が多数あります。後日思い出したらこの記事を徐々に改善していきます。
三、まとめ
AOT公開テストは途中で多くの問題に遭遇する可能性がありますが、タイムリーなテストと正しい設定調整により、最終的にプロジェクトのスムーズな公開を実現できます。上記の経験が皆さんのAOT使用時に役立ち、開発プロセスでの無駄を減らし、プロジェクトの開発効率と品質を向上させることを願っています。同時に、皆さんが実践の中で探求とまとめを続け、共に技術の進歩と発展を推進することを期待しています。
AOT参考プロジェクト:
- CodeWF.NetWeaver: https://github.com/dotnet9/CodeWF.NetWeaver
- CodeWF.Tools:https://github.com/dotnet9/CodeWF.Tools
- CodeWF.Toolbox:https://github.com/dotnet9/CodeWF.Toolbox