はじめに
前回記事: .NET 10 の LibraryImport を使いこなすうえでの注意点と変更点
本記事はその続編です。前回は「マネージドからネイティブを呼ぶ」側の細かい話に終始しましたが、今回はもう一歩踏み込み、「C++ で書かれたコアエンジンを持つアプリケーションの UI を、.NET 側(Avalonia)に置くという構成は現実的か?」 という問いを、技術選定・評価の観点から考えてみます。
この記事の要点
「C++ で書いた計算コアはそのまま活かし、UI だけを .NET 側に移したい」─ この構成は、もはや無理筋ではなくなってきたと思います。.NET 5 の UnmanagedCallersOnly、.NET 7 の LibraryImport、.NET 7〜10 で成熟した NativeAOT によって、C# は「C++ネイティブの世界に同居できる言語」になりました。
その UI 層に Avalonia を選ぶ理由も揃ってきました。MIT ライセンス、XAML への一本化、自前描画による全プラットフォーム同一の見た目 ── Qt の悩み(デュアルライセンス、Widgets と QML の分裂)も、MAUI の弱点(Linux/WASM 非対応)も、Avalonia は回避します。JetBrains や Autodesk の採用実績、Devolutions による 3 年 300 万ドルの出資が、継続性の裏づけです。
ただし注意点もあります。モバイルと WebAssembly はまだ発展途上、ホットリロードなど一部ツールは有償、C++ コアとの ABI 境界はやはり自前で設計する必要があります。
1. 前回の記事から自然に出てくる問い
前回記事では、DllImport から LibraryImport(ソース生成 P/Invoke) への移行、IL スタブの除去、AOT 互換性、StringMarshalling の挙動などを扱いました。
そこから自然に出てくる問いがあります:
「マネージド ⇄ ネイティブの境界が、ここまで安全に・AOT フレンドリーになったのなら、UI 層は .NET 側に置いて、計算カーネル(C++ エンジン)はネイティブ共有ライブラリとして直結する アーキテクチャも現実的なのではないか?」
これは「Qt で C++ から UI まで全部書く」「Electron で UI を JavaScript に逃がす」とは別の選択肢、すなわち C# UI プロセスを Avalonia で組み、C++ コアと狭い C-ABI で直結する という設計です。本記事ではその実現性を、技術選定・評価の観点から考えてみます。
2. C++/C# 接続性の進化 ― .NET 世代ごとに見る
2.1 主要 interop 機能のタイムライン
| バージョン | リリース年 | 機能 | 意義 |
|---|---|---|---|
| .NET Framework 〜 | 2002〜 |
DllImport(ランタイム IL スタブ生成) |
古典的な P/Invoke。AOT 不可、デバッグ困難 |
| .NET 5 / C# 9 | 2020 |
UnmanagedCallersOnly + 関数ポインタ delegate* unmanaged
|
マネージドメソッドをネイティブ関数ポインタとして直接公開可能に。デリゲート確保不要 |
| .NET 6 | 2021 | NativeAOT プレビュー、ComWrappers 改善 | |
| .NET 7 | 2022 |
LibraryImport(ソース生成 P/Invoke)正式導入、NativeAOT コンソールアプリ・共有ライブラリ対応 |
IL スタブ廃止、AOT 完全対応、生成コードをステップ実行可能 |
| .NET 8 | 2023 | NativeAOT 安定化、ASP.NET Core 対応、iOS / Mac Catalyst / Windows で NativeAOT サポート、出力サイズ大幅縮小 | |
| .NET 9 | 2024 | MAUI 文脈での iOS / Mac Catalyst NativeAOT 体験の品質向上・拡張 | モバイル・Apple プラットフォームの実用性向上 |
| .NET 10 | 2025 | マーシャリング改善、ネイティブライブラリ検索パスの変更、IsAotCompatible メタデータ強化 |
後述 |
注: 「.NET 9 で iOS / Mac Catalyst NativeAOT 正式サポート」と整理した記事をしばしば見かけますが、Microsoft .NET Blog「.NET MAUI Performance Improvements in .NET 9」によれば、.NET 8 時点で既に iOS / MacCatalyst / Windows の NativeAOT はサポート済み(「NativeAOT is not yet supported on Android, but is available on iOS, MacCatalyst, and Windows」)。.NET 9 は「実験 → 正式」のステップアップではなく、MAUI 文脈での品質向上・対応拡大 という表現が正確です。
2.2 LibraryImport(ソース生成 P/Invoke)
.NET 7 で導入され、DllImport のドロップイン置き換えとして設計されました。Microsoft Learn の説明によれば、「.NET 7 SDK に同梱されデフォルトで有効。LibraryImportAttribute を static partial メソッドに付与するとコンパイル時にマーシャリングコードが生成され、ランタイム IL スタブ生成が不要になり、P/Invoke がインライン化可能になる」とされています。
// 旧来: ランタイムが IL スタブを動的生成 → AOT 不可
[DllImport("engine", CharSet = CharSet.Unicode)]
private static extern int engine_compile(string source);
// 新: 生成コードが見える・ステップ実行できる・AOT 互換
[LibraryImport("engine", StringMarshalling = StringMarshalling.Utf8)]
private static partial int engine_compile(string source);
2.3 UnmanagedCallersOnly ― 「マネージドメソッドをネイティブ関数ポインタとして出す」
.NET 5 / C# 9 で導入されました。C# 9 の関数ポインタと組み合わさることで、C++ 側に渡すコールバックを、デリゲートのアロケーション無し・GC ピン留め無しで 提供できます。.NET ホスティング API(hostfxr)からのエントリポイント解決もこの属性に対応しています。
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
public static void OnEngineEvent(int code, IntPtr payload)
{
// C++ エンジンから直接呼ばれる
}
// C++ にこの関数ポインタを渡す
delegate* unmanaged[Cdecl]<int, IntPtr, void> cb = &OnEngineEvent;
engine_register_callback(cb);
static 限定、blittable 引数のみ、ジェネリック不可、マネージドコードから呼んではいけない、といった制約があります(Microsoft Learn「UnmanagedCallersOnlyAttribute Class」の規定そのまま)。これは「JIT 化された関数自体が GC トランジションを行う」という最適化を可能にするための制約です。
2.4 NativeAOT ― C# を共有ライブラリ(.so/.dll/.dylib)にする
.NET 7 で公式ゴールに「コンソールアプリと ネイティブライブラリシナリオ」が加わり、.NET 8 で大幅に成熟、Microsoft .NET Blog 系のレポートによれば .NET 8 時点で iOS / MacCatalyst / Windows の NativeAOT は既にサポート済み、Android については .NET 10 でも「ほぼ準備完了」の段階に到達という流れです。
ポイントは、C# プロジェクトを <PublishAot>true</PublishAot> + NativeLib=Shared で .dll/.so/.dylib として吐ける ことです。これは「C++ アプリ側が C# を 普通のネイティブ依存ライブラリ として dlopen/LoadLibrary できる」ことを意味します。
2.5 .NET 10 の interop 関連の変更点
.NET 10 では、interop 周りでいくつか重要な変更があります(Microsoft Learn の「Breaking changes in .NET 10」より):
-
ネイティブライブラリ検索パスの変更(Breaking): シングルファイルアプリで、実行ファイルディレクトリが自動的に
NATIVE_DLL_SEARCH_DIRECTORIESに追加されなくなりました。NativeAOT でも rpath のデフォルト設定が外れました。明示的にDllImportSearchPath.AssemblyDirectoryを指定する必要があります(Microsoft Learn「Breaking change - Single-file apps no longer look for native libraries in executable directory」)。 -
DllImportSearchPath.AssemblyDirectory単独指定時の挙動変更(Breaking): 以前はアセンブリディレクトリを探した後 OS のデフォルト検索パスにフォールバックしていましたが、.NET 10 からは アセンブリディレクトリのみ を探します(Microsoft Learn「Breaking change - Specifying DllImportSearchPath.AssemblyDirectory only searches the assembly directory」)。 -
NativeAOT 共有ライブラリの
AppContext.BaseDirectory変更: 共有ライブラリのファイル位置を指すように変更(Microsoft の互換性ドキュメントを要参照)。 -
IsAotCompatibleメタデータ: ライブラリプロジェクトで<IsAotCompatible>true</IsAotCompatible>を指定すると、トリミング・AOT 解析が一括で有効化されます。.NET 10 ではこのメタデータをアセンブリに付与する仕組みが強化され、依存先がアノテーション付きかチェックできるようになりました(警告 ID の詳細は Microsoft Learn の該当ドキュメントを参照)。
これらは「C++ 側から dlopen する .NET 共有ライブラリ」「C# 側に C++ ネイティブをバンドルしたシングルファイルアプリ」のどちらの方向でも、ロード挙動が変わるので 移行時に必ず確認すべき ポイントです。
2.6 実例: C# Dev Kit の C++ Node.js アドオン置き換え
「ここまでできるなら現実に置き換えた人はいるのか?」という問いに対する直近の好例が、Microsoft 自身による事例です。Microsoft .NET Blog の "Writing Node.js addons with .NET Native AOT"(2026 年 4 月 20 日、Drew Noakes 氏(Principal Software Engineer)による寄稿) は、C# Dev Kit チーム が VS Code 拡張で長年使ってきた C++ で書かれた Node.js ネイティブアドオン(node-gyp + Python 経由でビルド)を、.NET 10 + NativeAOT + [LibraryImport] + [UnmanagedCallersOnly] + N-API に置き換えた話を詳述しています。
ターゲットは net10.0、<PublishAot>true</PublishAot>。dotnet publish で生成された共有ライブラリ(.dll/.so/.dylib)を .node にリネームして Node.js から読み込んでいます。napi_register_module_v1 や個別コールバックを [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] で公開し、napi_create_string_utf8、napi_create_function、napi_get_cb_info などを [LibraryImport("node")] で宣言しています。
著者自身の言葉:
「The C# Dev Kit team already has the .NET SDK installed, so why not use C# and Native AOT to streamline our engineering systems?」
「Performance has been comparable to the C++ implementation.」
「Native AOT increases the number of places you can run your .NET code.」
― Drew Noakes, Writing Node.js addons with .NET Native AOT, Microsoft .NET Blog (April 20, 2026)
ビルドの Python 依存が消え、CI が単純化、コントリビュータの onboarding が改善した、というのが主要な成果として述べられています。これは UI フレームワークの選択以前の重要なメッセージです: Microsoft 自身が、自社の本番フロー上で C++ ネイティブモジュールを C# NativeAOT で置き換える ところまで来た、という生きた検証材料です。
2.7 hostfxr / nethost vs C++/CLI
C++ から .NET を「埋め込む」古典的な選択肢は C++/CLI でしたが、これは Windows / MSVC 限定 で、Linux/macOS に持っていけません。
現代の方法は以下のとおりです:
| 方式 | 何ができるか | 特徴 |
|---|---|---|
| C++/CLI | C++ コードに /clr を付け、マネージドと混在 |
Windows 限定。クロスプラットフォーム不可 |
| hostfxr / nethost(ネイティブホスティング API) | C++ プロセスから .NET ランタイムを動的に起動し、マネージドメソッドへの関数ポインタを取得 | クロスプラットフォーム。フレームワーク依存配置が前提 |
| NativeAOT 共有ライブラリ | C# 側を .so/.dll/.dylib としてビルドし、C++ 側は普通の P/Invoke 的に呼ぶ |
クロスプラットフォーム、JIT 不要、起動高速。リフレクション等に制約あり |
nethost の get_hostfxr_path() → hostfxr_initialize_for_runtime_config → hostfxr_get_runtime_delegate で UnmanagedCallersOnly 付きメソッドへの関数ポインタを直接得られ、デリゲート型名すら指定不要です。
本記事の文脈 で重要なのは:
- 「C# UI(Avalonia)プロセスから C++ コアを呼ぶ」なら →
LibraryImport+UnmanagedCallersOnlyで十分(マネージドが「ホスト」) - 「既存 C++ アプリの中に C# UI 部品を埋め込む」なら →
nethost/hostfxrか NativeAOT 共有ライブラリ - どちらの方向にも「狭い C-ABI 境界」を設計する点は共通
2.8 .NET 11 で何が変わるか(2026年11月予定)
ここまで .NET 5 から .NET 10 までの interop 進化を見てきました。次に控える .NET 11(2026年11月 GA 予定、STS リリース) で何が来るか、本記事の文脈に関わる範囲で整理しておきます。執筆時点では Preview 4 まで公開されています。
C++ interop 関連: 大きな新機能はないが、洗練が一つ
新しい属性や新しい P/Invoke の枠組みが導入される予定はありません。これは「interop の革命は .NET 5(UnmanagedCallersOnly)→ .NET 7(LibraryImport)→ .NET 8(NativeAOT 安定化)で一段落した」という事情によるもので、既存の LibraryImport / UnmanagedCallersOnly ベースで設計したコードはそのまま素直に .NET 11 へ持っていけます。
唯一、本記事の文脈に直接効く改善が C# 14 の [StructLayout] 拡張 です。struct 型のフィールドのメモリ上の配置をより細かく制御できるようになり、C++ 側の POD 構造体と寸分違わず合わせ込む作業が楽になる見込みです。4章で述べた「狭い C-ABI 境界に POD 構造体を渡す」設計と、相性のよい変更です。
NativeAOT は引き続き顕著に進化
interop と比べると、NativeAOT のほうは明確に攻めの投資が続いています。本記事に関わるものを並べると:
- MAUI のモバイルランタイムが CoreCLR デフォルトに(Preview 4、2026年5月13日発表)。.NET MAUI が Android・iOS・Mac Catalyst で Mono ではなく CoreCLR で動くデフォルトになることで、モバイルアプリにも NativeAOT が現実的な選択肢として開ける(Microsoft .NET Blog「.NET MAUI Moves to CoreCLR in .NET 11」)。なお、サードパーティ報道(windowsnews.ai)によれば、リファレンス e-commerce アプリでのパイロット計測で「CoreCLR + NativeAOT でメモリ使用量 30%減、time-to-interaction 40%減」という早期数値も伝えられています(Microsoft の一次発表記事の本文にはこの具体値は含まれていないため、出典は二次報道として扱うのが妥当)。
- iOS など JIT 非対応プラットフォームのインターフェースディスパッチが、キャッシュ済みディスパッチで大幅に改善されたと .NET 11 Preview リリースノート群で報告されています(具体倍率は Microsoft 一次資料を直接参照のこと)。NativeAOT で iOS をターゲットにする現実性が一段と上がります。
- R2R が
Comparer<T>.Default等を NativeAOT 流の特殊化ヘルパーで生成(コレクション操作の改善)。R2R と NativeAOT の最適化アプローチが収束してきています。 -
Runtime Asyncが NativeAOT 対応(Preview 1 アナウンスで「runtime-level async mechanism (including configuration, diagnostics, and AOT support)」)。非同期コードを AOT コンパイル後でも動作可能に。 - NativeAOT で WASM をパブリッシュする際、NuGet パッケージのサテライトアセンブリが落ちなくなる(ローカリゼーション周りのバグ修正)、WASM の CoreCLR ビルドで Emscripten ベースのフルパイプラインがネイティブ再リンクをサポート。WASM 周りの土台が整ってきています。
- dotnet CLI 自体を NativeAOT エントリポイント化する基盤工事(.NET 11 SDK の方向性として言及されている。詳細は SDK のリリースノートを要確認)。Microsoft が自社のツール群を AOT に寄せていく意思表示でもあります。
本記事のアーキテクチャへの影響
.NET 11 は「インフラ寄り、深く広く」の地味なリリースで、.NET 10 のような派手な新機能の塊ではありません。複数のメディアが「.NET 10 = broad、.NET 11 = deep」「.NET 11 は .NET 10 の延長線上にあり、リプレースではない」と評しています。
ただし、その方向性は本記事の主張(Avalonia + LibraryImport + UnmanagedCallersOnly + NativeAOT で C++ コアを直結する)を強化するものです。デスクトップ向けに書いた構成がそのまま動き続けるのに加えて、
- モバイルでも NativeAOT が選択肢に入る
- iOS のインターフェースディスパッチコストが下がる
- WASM のパッケージング問題が解消される
という具合に、6章のデメリット側で挙げた「モバイル / WASM はまだ発展途上」の弱点が、.NET 11 で目に見えて埋まっていく見通しです。
サポート種別の観点では、.NET 9 から STS のサポート期間が 18 ヶ月 → 24 ヶ月 に延長されました(Microsoft .NET Blog「.NET STS releases supported for 24 months」、2025年9月16日)。これにより:
- .NET 10 LTS: 〜2028年11月14日(Microsoft 公式 Lifecycle ページに準拠)
- .NET 11 STS: 2026年11月 GA + 24ヶ月 = 〜2028年11月
と、End of Support がほぼ揃います。エンタープライズ志向で安定運用を優先するなら .NET 10 LTS のまま、新機能(特にモバイル AOT)を早期に取り込みたいなら .NET 11 STS、という選択になります。本記事のアーキテクチャはいずれでも成立します。
3. UI フレームワークの選択肢: Avalonia vs Qt vs MAUI
3.1 Avalonia のアーキテクチャ(概要)
- Skia ベースの自前描画(将来は Google の Impeller バックエンドへ移行作業中。NImpeller として .NET バインディングは既に公開、ただし Avalonia のフルレンダリングバックエンドは UXDivers のレビューによれば private internal branch で開発中)
-
WPF inspired な XAML + MVVM、
x:CompileBindingsによるコンパイル時バインディング - MIT ライセンス
- Windows / macOS / Linux / iOS / Android / WebAssembly をターゲット(モバイル・WASM は後述のとおり成熟度に差がある)
「自前描画」というのは Flutter と同じ哲学で、OS のネイティブコントロールをラップせず、ピクセルを自分で描く。結果として「Windows と macOS と Linux で見た目・挙動が同じ」「テーマを統一しやすい」「IDE のような密度の高い独自 UI を作りやすい」という性質になります。
3.2 Qt との比較
| 観点 | Qt | Avalonia |
|---|---|---|
| ライセンス | LGPL v3 / 商用デュアル。静的リンクや一部モジュール(Qt Charts、Virtual Keyboard、Wayland Compositor、Safe Renderer、Qt for MCU 等)は実質商用必須。エンドユーザが Qt ライブラリを差し替え可能にする義務など、LGPL 遵守が法務レビューで重荷になりがち | MIT 一本。商用利用も静的リンクも自由。Avalonia 本体に統合された有償オプション(Pro)あり |
| コスト | 公式 qt.io の小規模事業者向けプランで「Qt for Application Development Enterprise: 546 EUR/年」(qt.io/development/qt-for-small-business)。Qt for Device Creation の標準価格は二次資料(embeddeduse.com の試算、2023年1月)で 約 5,500 EUR / 開発者 / 年 と試算されている。大規模・組込みではさらに増 | コアは無償。ツール群が有償(後述) |
| 言語/エコシステム | C++、QML、Qt Quick、Qt Widgets(Widgets と QML の二系統に分裂しており、選択と移行が悩ましい) | C# + XAML 一本 |
| UI 記述 | QML(JavaScript ライクな宣言型)と Widgets(C++ オブジェクトツリー)で分裂 | XAML で一貫。WPF/UWP/WinUI からの移行コストが小さい |
| 得意領域 | 深い組み込み・MCU・ベアメタル(Qt for MCU、Qt Quick Ultralite)、機能安全(Qt Safe Renderer は ISO 26262 ASIL D / IEC 61508 SIL 3 / EN 50128 SIL 4(鉄道)/ IEC 62304(医療機器、Class C / fit-for-use) で認証取得済み)、自動車インパネ、25 年超の実績 | デスクトップ、密度の高い IDE 風 UI、クロスプラットフォーム一貫性、Linux の主要選択肢 |
| モバイル | 長年のノウハウあり | Android / iOS は「機能としては動くが Avalonia の主投資領域は依然デスクトップ」(UXDivers の評価) |
結論: ライセンス・コスト・UI 記述の一貫性で Avalonia は明確に有利。逆に 既に Qt で長期サポート契約を結んでいる組込み案件 では Qt のままが正解です。
「Qt から Avalonia へのポート」議論は実際に GitHub Discussions(AvaloniaUI/Avalonia#13588 "Porting from Qt to Avalonia")で観察できます。「Qt Widgets + WebSocket + 動的レイアウト」のような構成は、DataTemplate と MVVM で素直に書き直せる場合が多い、というのが現場の所感です。一方、QML スクリプトに密結合した UI ロジック はそのまま移植する手段が無く、C# 側に書き直す必要があります。
3.3 MAUI との比較
| 観点 | .NET MAUI | Avalonia |
|---|---|---|
| レンダリング | ネイティブコントロールをラップ(Handler アーキテクチャ) | 自前描画(Skia / 将来 Impeller) |
| 対応プラットフォーム | Android、iOS、Mac Catalyst、Windows(WinUI 3)。Linux は公式非対応、WebAssembly も非対応 | Windows、macOS、Linux、iOS、Android、WebAssembly |
| 見た目の一貫性 | プラットフォームごとにネイティブの見た目(モバイルでは自然) | 全プラットフォームで同一のピクセル(IDE 系・ブランド統一系に有利) |
| サポート期間 | Microsoft 公式 MAUI サポートポリシーでは「メジャー版は 次メジャーリリース後 6 ヶ月 までサポート」 | LTS 的な明示は無いが、自社開発で更新は継続。Devolutions の 3 年スポンサーシップで予測可能性は上昇 |
| breaking change の体感 | 初期に指摘多数。.NET 8/9/10 で改善傾向だが「毎年の大改修」感は残る | 公式が「stability is a feature(安定性こそ機能)」を標榜 |
| モバイルのネイティブ感・公式性 | Microsoft 公式、ネイティブな操作感 | 自前描画のためモバイルでは「ネイティブには見えない」 |
2025 年 11 月発表: Avalonia MAUI Backend
2025 年 11 月 11 日、AvaloniaUI 社が 「.NET MAUI を Avalonia レンダラで動かす」 バックエンドの開発を公式発表しました(avaloniaui.net/blog/net-maui-is-coming-to-linux-and-the-browser-powered-by-avalonia)。要点は以下のとおり:
- MAUI のコードベース(Handler 設計)を維持しつつ、レンダリング層を Avalonia に差し替える
- これにより MAUI アプリが Linux と WebAssembly に展開可能になる(MAUI 側でずっと要望が出ていた領域)
- macOS では「Mac Catalyst ではなく Avalonia レンダラ」になることでパフォーマンス改善の可能性
- 「MAUI エコシステムのエンジニアからのガイダンスとフィードバック」を受けて開発されたとアナウンス(原文:「with guidance and feedback from engineers in the MAUI ecosystem」)
- Preview のサインアップを受付中。The Register(2025年11月13日報道) によれば「nothing further beyond a signup for preview access promised in the first quarter of 2026」── すなわち 2026 年第 1 四半期にプレビューアクセスが提供される予定 とされている(Avalonia 公式ブログ本文では具体時期は明示されておらず、二次報道に基づく)
The Register は Microsoft 側の姿勢について「James said that this has been done 'with guidance and feedback from engineers in the MAUI ecosystem,' implying though not quite stating that the Microsoft developers are supportive」── すなわち「Microsoft の開発者が支持していることを 明言はしないものの示唆している」と表現しています。Microsoft が公式に Linux MAUI を出さない以上、Avalonia がその穴を埋める形で 共存 している、というのが現状の構図です。
MAUI ではなく Avalonia を選ぶべき場面:
- Linux 対応が要件にある
- WebAssembly 配信も視野に入る
- IDE 風・密度の高いデスクトップ UI を作る
- ピクセル一貫性が重要(ブランド要件、UI テスト自動化)
- breaking change を最小化したい
MAUI のままが良い場面:
- iOS / Android が主戦場で、ネイティブな見た目・操作感が重要
- Microsoft の公式サポート契約・SLA が必要
- Windows ストア配布・WinUI 3 機能をフル活用したい
3.4 Compiled Bindings ── XAML を NativeAOT に乗せる仕組み
3.1 で Avalonia の特徴として x:CompileBindings を挙げました。これは本記事の ── 「LibraryImport + NativeAOT で C++ コアを直結する」というアーキテクチャを UI 側でも貫徹できるか ── に直結する要素なので、もう少し説明します。
従来の {Binding} の問題
WPF / Xamarin の伝統的な {Binding ViewModel.UserName} は、実行時に DataContext の型をリフレクションで調べ、文字列 "UserName" に一致するプロパティを探し、INotifyPropertyChanged を購読する、という動作をします。柔軟ですが、いくつかコストがあります。
- プロパティ名のタイプミスは実行時まで気付けない(Output ウィンドウに警告が出るだけで、UI は静かに空欄になる)
- リファクタリングでプロパティ名を変えると、バインディングが黙って壊れる
- 毎回リフレクションが走るため、バインディング解決コストが嵩む
- 決定的に、トリミングと NativeAOT に致命的に弱い。リフレクションでアクセスされるプロパティは静的解析で追えないため、トリマーが「未使用」と誤判定して削除したり、AOT コンパイル時にメタデータを残せなかったりする
最後の点が本記事のテーマと直接ぶつかります。.NET 7 以降で LibraryImport と NativeAOT を使って C++ コアと直結する設計を選んでも、UI 側のバインディングがリフレクション依存だと、結局 C# プロセス全体を NativeAOT で publish できません。
Compiled Bindings(コンパイル時バインディング)とは
Avalonia の Compiled Bindings は、XAML 側で DataContext の型を宣言 することで、XAML コンパイラがバインディングを 強く型付けされた直接アクセスコード に変換する仕組みです。
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:vm="using:MyApp.ViewModels"
x:CompileBindings="True"
x:DataType="vm:MainViewModel">
<!-- {Binding UserName} は、リフレクションではなく
MainViewModel.UserName への直接アクセスとしてコンパイルされる -->
<TextBlock Text="{Binding UserName}" />
</UserControl>
これにより以下が得られます。
-
UserNameのタイプミスはコンパイルエラーになる - IDE のリネームリファクタリングが効く
- 実行時のリフレクションコストが消える
- トリマーと NativeAOT が、プロパティアクセスを静的に追える
プロジェクト全体でデフォルト ON にする場合は .csproj に次を入れます。
<PropertyGroup>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
明示的に区別したい場合は {CompiledBinding UserName} という別マーカーも使えますが、x:CompileBindings="True" の配下では {Binding} がそのまま compiled binding として扱われます。なお Avalonia 12 では Avalonia.props 経由で Compiled Bindings が 既定で有効化 されているため、新規プロジェクトでは特別な設定なしにこの恩恵を受けられます。
なぜこれが「Avalonia は AOT に強い」の根拠になるのか
Compiled Bindings は構文上はささやかな機能に見えますが、Avalonia が NativeAOT を現実的なターゲットとして主張できる理由の中核 です。WinUI 3 / UWP の {x:Bind} が同じ思想で、Microsoft 自身も公式ガイダンスで「リフレクションベースの {Binding} は古典的、{x:Bind} がモダン」という整理をしています。Avalonia はそれを WPF 互換の XAML 構文を保ったまま実現しました。
これにより、C# UI プロセス全体を NativeAOT で publish したまま C++ コアと直結する という、本記事のアーキテクチャの前提が現実的に成立します。
| レイヤ | NativeAOT 適性の根拠 |
|---|---|
| C# → C++ 呼び出し |
[LibraryImport] がソース生成、ランタイム IL スタブ不要 |
| C++ → C# コールバック |
[UnmanagedCallersOnly] + 関数ポインタ、デリゲートのリフレクション不要 |
| C# UI のデータバインディング | x:CompileBindings で XAML がリフレクションを使わない |
| C# ロジック全般 | 通常の AOT 互換性ルール(IsAotCompatible 等) |
「C++ 接続部分は AOT 対応だが、UI 層がリフレクション依存で publish 時にトリミング警告だらけになる」── という、WPF 系の伝統的なフレームワークが抱えがちな問題を、Avalonia は構造的に回避できる設計になっています。
実務上のデメリット
公平に挙げておくと、Compiled Bindings には規律としてのコストがあります。
-
x:DataTypeを明示する必要があり、DataTemplate内でも再宣言するなど 記述量が少し増える - 動的な DataContext 切り替え(同じ View に複数種類の ViewModel を流し込む等)はやりにくくなる
- WPF からの移植時、
{Binding}を一律に compiled に切り替えると 既存コードの暗黙の依存 が壊れる箇所がある
ただし、いずれも「DataContext の型を明示する」という MVVM の規律強化と捉えるのが妥当で、本記事が想定するような新規・本格運用のプロジェクトではむしろ歓迎すべき制約です。WPF からの移行プロジェクトでは、AvaloniaUseCompiledBindingsByDefault を false にしておき、View ごとに段階的に opt-in する戦略が現実的です。
4. アーキテクチャ設計: 狭い C-ABI 境界を作る
実際に「C++ エンジン + Avalonia UI」を組むときのアーキテクチャ案:
┌────────────────────────────────────────────┐
│ C# プロセス (Avalonia UI, ホスト) │
│ ├─ View / ViewModel (XAML, MVVM) │
│ ├─ Dispatcher.UIThread │ <- 自作デバッカ&波形表示ツール
│ └─ Engine ラッパ │ ( SukimaDebug )
│ │ │
│ ▼ LibraryImport (forward call) │
│ ─────────── 狭い C-ABI 境界 ────────────────│
│ ▲ UnmanagedCallersOnly (callback) │
│ │ │
│ C++ コアエンジン (.so / .dll / .dylib) │ <- 自作SystemVerilogシミュレータ
│ ├─ AST / ネットリスト / シミュレータ │ ( sukimasim )
│ └─ blittable な C 構造体のみ公開 │
└────────────────────────────────────────────┘
4.1 設計原則
-
境界は C-ABI(
extern "C")に限定する。C++ のクラス、テンプレート、std::string、std::vectorを境界に出さない。 -
境界に出す型は blittable に。
int32_t、ポインタ、固定レイアウトの POD 構造体。文字列は UTF-8 でconst char*+ 長さ(StringMarshalling.Utf8と一致させる)。 -
複雑なオブジェクトグラフ(AST、ネットリスト)はハンドル化。C++ 側で所有し、C# には不透明な
IntPtr(またはSafeHandle)だけを渡す。 -
コールバックは
[UnmanagedCallersOnly]+ 関数ポインタ。デリゲート +Marshal.GetFunctionPointerForDelegateは GC ピン留めの罠が多く、AOT 互換性も悪い。 -
UI スレッドへの post は
Dispatcher.UIThread.Post。C++ から呼ばれるコールバックは任意スレッドで実行されるため、Avalonia の UI スレッドへ明示的にマーシャリングする必要があります。
4.2 コード例(要点のみ)
// C# → C++ (forward)
internal static partial class Engine
{
[LibraryImport("engine", StringMarshalling = StringMarshalling.Utf8)]
internal static partial IntPtr engine_compile(string source);
[LibraryImport("engine")]
internal static partial void engine_dispose(IntPtr handle);
[LibraryImport("engine")]
internal static unsafe partial void engine_set_progress_callback(
IntPtr handle,
delegate* unmanaged[Cdecl]<int, IntPtr, void> cb,
IntPtr userData);
}
// C++ ← C# (callback, AOT セーフ)
file static class EngineCallbacks
{
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
public static void OnProgress(int percent, IntPtr userData)
{
// !! ここは任意スレッド。UI 触る前に必ず Post する
Dispatcher.UIThread.Post(() =>
{
// ViewModel 更新
});
}
}
C++ 側:
extern "C" {
EngineHandle* engine_compile(const char* source_utf8);
void engine_dispose(EngineHandle* h);
void engine_set_progress_callback(
EngineHandle* h,
void (*cb)(int, void*),
void* user_data);
}
4.3 開発環境とツールチェーン
C++ と C# の混在プロジェクトの開発環境ですが、C++ 側と C# 側をひとつの IDE に無理に統合しようとしない方がよいと考えています。本記事で提案する「狭い C-ABI 境界」は、ビルドと IDE の面でも効いてきます。境界が薄く、両者が独立してビルドできるなら、それぞれを最適な環境で開けると思うからです。
しかし、デバックはちょっとやっかいです。
プロジェクト構成
- C++ コアエンジン: 独立した CMake プロジェクト とする。CMake は Windows / Linux / macOS 共通の lingua franca であり、CLion・Visual Studio・VS Code のいずれからも扱える。
- C# / Avalonia UI: Rider または Visual Studio で開く。Avalonia のプレビューア・XAML 補完などツーリングは Rider が最も充実しているため、UI 側は Rider が無難。
-
ビルドの連結: C# の
.csprojから CMake をターゲットとして呼び出し、C# ソリューションのビルド時に C++ 側が pre-build で自動ビルドされるようにする。JetBrains の公式フォーラムでも、この「.csprojから CMake CLI を実行する」方式が現実解として案内されている。
<!-- C# 側 .csproj: C++ エンジンを pre-build でビルド -->
<Target Name="BuildNativeEngine" BeforeTargets="Build">
<Exec Command="cmake --preset release" WorkingDirectory="../engine" />
<Exec Command="cmake --build --preset release" WorkingDirectory="../engine" />
</Target>
混在デバッグ ── OS で事情が変わる
混在プロジェクトで開発環境の良し悪しを決めるのは mixed-mode debugging(マネージドとネイティブを 1 セッションで追える機能) であり、ここは OS によって状況が大きく異なります。
| 環境 | 混在デバッグ | 備考 |
|---|---|---|
| Windows + Visual Studio | 成熟。マネージド/ネイティブを 1 セッションで、境界をまたいでステップ実行可。Hex メモリビューアあり |
LibraryImport のマーシャリングバグ追跡に最適 |
| Windows + Rider 2026.1 | 対応(Windows のみ)。.NET とネイティブを 1 セッションでデバッグ可。CMake プロジェクトの Beta サポートも追加 | クロスプラットフォーム IDE 一本化の現実解 |
| Linux / macOS(全 IDE) | .NET ランタイム対象の混在デバッグは未対応 | C# 側と C++ 側を別デバッガ(lldb/gdb)で分離して追う |
| VS Code | 弱い。混在デバッグは事実上非対応 | 編集は C# Dev Kit + C/C++ 拡張で可能だが、メインには据えにくい |
Windows をメイン開発機にする場合、Visual Studio が今でも最も快適 です。マーシャリングのバグ ── 構造体が unmanaged 側へ正しく渡っているか ── を追うとき、「境界をまたいでステップ実行できる + メモリを直接見られる」環境が決定的に効きます。
Rider もこの領域で急速に追いついています。JetBrains 公式ブログ「Rider 2026.1: More AI Choice, Stronger .NET Tooling, and Expanded Game Dev Support」(2026年3月30日)によれば、Rider 2026.1 では file-based C# プログラムへの対応、Windows での MAUI 開発体験の改善、混在モードデバッグ、そして CMake プロジェクトの Beta サポート が導入されました。Windows での混在モードデバッグについて公式ブログは「Rider 2026.1 introduces mixed-mode debugging... .NET mixed-mode debugging is currently available only on Windows」と明記しています。クロスプラットフォーム IDE を一本化したいなら、Rider 2026.1 以降が現実的な選択肢になりつつあります。
ただし注意点として、Linux / macOS で .NET ランタイムを対象とした混在デバッグは、まだ実用段階にありません。この機能は Windows 限定の Visual Studio では既に利用できますが、クロスプラットフォームの環境(VS Code を含む)にはありません。Linux/macOS では、C# 側はマネージドデバッガ、C++ 側は lldb/gdb を別々にアタッチして追う「分離デバッグ」が現実的な運用になります。
推奨構成のまとめ
- マーシャリング・ABI のデバッグが頻繁に発生すると見込むなら → Windows + Visual Studio を「重いデバッグ用の母艦」に据えるのがよさそう
- クロスプラットフォームで IDE を一本化したいなら → Rider 2026.1 以降(CMake Beta + Windows 混在デバッグ)。ただし Linux/macOS の混在デバッグは分離運用で割り切るしかなさそう
- VS Code は軽量で編集には使えるが、混在デバッグが弱いためメイン IDE には不向き
いずれの構成でも、C++ と C# を別プロジェクトとして保ち、CMake と dotnet という別ビルドシステムを pre-build 連結で橋渡しする という原則は共通です。これは 4.1 で述べた「狭い C-ABI 境界」の方針が、コードだけでなく開発ワークフローにもそのまま当てはまることを意味します。
4.4 Linux 上での起動プロセス
「Avalonia + C++ で Linux 上にアプリを作ったとき、どちらのプロセスが先に起動するのか?」── これは混在構成を初めて設計するときに出る疑問だと思います。結論から言えば、この構成の場合は C++ 側は独立したプロセスにはなりません。本記事のアーキテクチャでは、C++ エンジンは .so 共有ライブラリとして C# プロセスに動的ロードされるため、OS から見て最初に立ち上がるのは C# / Avalonia プロセスの方です。
起動シーケンス
ポイントを整理すると:
- OS が起動するのは C# / Avalonia の実行ファイル。NativeAOT で publish した ELF バイナリ、または
dotnet myapp.dll経由の .NET ホストプロセス。 - .NET ランタイム初期化 →
Main()→ Avalonia ブートストラップ (X11 または Wayland 接続、初期 XAML ロード)までは、C++ エンジンは一切メモリに載っていません。 - C# 側で最初に
[LibraryImport("engine")]の関数が呼ばれた瞬間に、dynamic linker(ld-linux.so)がdlopen("libengine.so")を実行 し、共有ライブラリがプロセスの仮想アドレス空間にマップされます。 - ロードと同時に C++ 側の static 初期化子 (
staticオブジェクトのコンストラクタ、__attribute__((constructor))関数、グローバル変数のコンストラクタ)が実行されます。 - それ以降、C++ コードと C# コードは 同一プロセス・同一アドレス空間 で同居し、関数呼び出しは通常の関数ポインタ呼び出しと変わらないコストで行き来します。
ライブラリのロード状態
C++ エンジンの「いつメモリに載るか」という観点では、次の3状態を意識すると見通しがよくなります。
デフォルトの lazy load(初回 P/Invoke で読み込む)挙動は、起動を軽くしたい用途には合っていますが、エンジン側に「起動直後に走らせたい初期化」があるケースでは不利です。具体的には:
- 高精度タイマや計測機構のキャリブレーション
- 共有メモリセグメント、巨大バッファ、メモリプールの確保
- ライセンス検証、認証トークンの取得
- ホットパス用のテーブル生成・JIT コンパイル
これらを抱えるエンジンを lazy load のままにすると、ユーザーが UI を操作してエンジン機能を初めて触った瞬間に重い初期化が走り、UI が固まったように見えます。これを避けたい場合は、Program.Main() の早い段階で明示的に eager load させます。
public static class Program
{
[STAThread]
public static void Main(string[] args)
{
// C++ エンジンを Avalonia 起動前に明示的にロードして
// static 初期化を済ませておく
NativeLibrary.Load("engine",
typeof(Program).Assembly,
DllImportSearchPath.AssemblyDirectory);
// あるいは軽量な init 関数を一度叩く方式でも可
// Engine.engine_init();
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
public static AppBuilder BuildAvaloniaApp() =>
AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace();
}
libengine.so の配置と検索パス
dynamic linker が libengine.so をどこから探すかは、Linux では RPATH / RUNPATH、LD_LIBRARY_PATH、/etc/ld.so.conf 系列で決まります。2.5 で触れた .NET 10 のネイティブライブラリ検索パス変更(シングルファイルアプリで実行ファイルディレクトリが自動追加されなくなった件、DllImportSearchPath.AssemblyDirectory 単独指定時の OS フォールバック除去)が直接効いてくるのもここです。
実務上の堅実な選択肢は次のいずれかです:
-
publish 出力ディレクトリに
libengine.soを同梱 し、[DefaultDllImportSearchPaths(DllImportSearchPath.AssemblyDirectory)]を明示する -
NativeLibrary.SetDllImportResolverで完全にカスタム解決ロジックを書く(マルチプラットフォーム配布や、検索順序を厳格に制御したい場合) -
RPATH=$ORIGINを C# 側 ELF に焼き込んで、実行ファイル相対で.soを引かせる(NativeAOT 時のオプション)
IPC モデルを選んだ場合の起動順は別の話
上のシーケンスは「同一プロセス内に直結する」前提です。もし要件上 UI プロセスとエンジンプロセスを分ける 設計を採る場合(クラッシュ分離、複数 UI からの共有、サーバ運用との親和性などが理由)は、起動の話が変わります:
-
UI 起動時にスポーン方式: C# Avalonia プロセスがユーザー操作で起動し、必要に応じて
Process.Startでエンジンプロセスを起こす - 常駐デーモン方式: エンジンを systemd ユニットなどで先に常駐起動しておき、UI が後から UNIX ドメインソケットや gRPC でつなぎに行く
どちらも複雑度と境界オーバーヘッドが増えるため、シンプルなデスクトップアプリでは過剰な設計です。本記事のメインアーキテクチャ(4.1 の「狭い C-ABI 境界 + 同一プロセス」)では、起動順について悩む必要はありません ── C# が起動し、C++ は後から(あるいは明示的に eager に)呼び出されてプロセスに同居する、というシンプルな構図です。
5. 実績と開発継続性の評価
技術選定における「数年後も生きているか」の評価軸:
5.1 採用実績
- JetBrains: 公式ガイドで「Rider は Avalonia のための唯一のクロスプラットフォーム IDE」(原文:「JetBrains Rider: The only cross-platform IDE for Avalonia」)と自社訴求(jetbrains.com/lp/rider-avalonia/)
- Autodesk、Unity、JetBrains、Devolutions、Schneider Electric、GitHub、NASA などが Avalonia 公式サイト・LinkedIn ページの本番採用事例として挙げられている(LinkedIn 表現:「Used by industry leaders, including JetBrains, GitHub, Unity and Schneider Electric」、公式 About:「used by NASA, JetBrains, Unity, and thousands of teams」)
- GritWorld(GritGene 3D エンジン) の事例が JetBrains 公式ブログ「Case Study: How GritWorld Uses Rider and Avalonia to Build a Powerful 3D Engine」(2021年5月10日)で紹介されている。Wasabi Wallet も JetBrains インタビューで取り上げられている
5.2 商業的 backing と資金面
- AvaloniaUI OÜ(エストニア・タリン本拠の法人)が中核チームを運営。CEO Mike James 氏は 2025 年 6 月時点でチーム規模 13 名(DEVCLASS 報道:「the business has a team of 13 people, now set to be increased」)、その後の採用拡大により公式 About ページでは 「a remote-first team of 19, headquartered in Tallinn, with Avalonians across 11 countries」 と記載されている
- エンタープライズ向け support plan を提供
- 2025 年 7 月 1 日、Devolutions が 3 年で 300 万米ドル(USD 3 million / 3 years)のスポンサーシップ を発表(GlobeNewswire / Devolutions プレスリリース、Avalonia 公式 Discussion #19108)
- スポンサーシップは DEVCLASS の取材報道(2025-06-26) によれば「The sponsorship is conditional on Avalonia code remaining under the MIT licence」とされており、コアが MIT のままであることが条件
- Devolutions CEO David Hervieux 氏の公式コメントは「Avalonia gives us the performance and flexibility to build robust, cross-platform PAM solutions without compromise」(GlobeNewswire)
- AvaloniaUI 側の CEO Mike James 氏は「revenue had increased since 2023, including 69 percent growth in the first half of 2025 alone」と DEVCLASS に語っている
5.3 コミュニティへの貢献者
Avalonia 公式や JetBrains インタビュー記事等で、コア OSS への貢献者として Microsoft / JetBrains 等の名前が言及されています(具体的な企業リストは公式 About ページの最新版を参照ください)。
5.4 .NET Foundation 離脱(2024 年 2 月 20 日)
- 2020 年 4 月 1 日に加入、2024 年 2 月 20 日に離脱(Wikipedia, AvaloniaUI/Avalonia Discussion #14666 "Farewell to the .NET Foundation")
- 離脱理由(コアチーム署名による公式 Discussion #14666): Foundation との interaction や benefit が限定的であった旨をコアチームが説明。DEVCLASS は「a notable lack of interaction and support from the Foundation」と要約している
- 離脱後はオーナーシップを Estonia 法人 AvaloniaUI OÜ に集約し、独立運営
これは見方によっては「.NET 公式から距離を置いたリスク」とも取れますが、Microsoft 側との関係は実用レベルで継続 しています(Avalonia MAUI Backend での MAUI チームとの協業、CEO の Mike James 氏自身が Microsoft の developer relations team 出身、など)。Microsoft は MAUI 公式という建前を維持しつつ、Avalonia の存在を 共存 させている、というのが妥当な読み方です。
5.5 ロードマップ
-
Avalonia 12(2026 年 4 月 7 日リリース、本記事執筆時点で 12.0.3): Avalonia 公式ブログ「Avalonia 12 - Ready for What's Next」によれば、NativeAOT 使用時で起動時間が 1,960ms → 460ms と 4 倍改善、スクロールは 42 → 120 FPS に到達。ページベースナビゲーション(
ContentPage,DrawerPage,TabbedPage+TabView,PipsPager)、AT-SPI2 アクセシビリティ、WebView オープンソース化、複雑なシーンで最大 1,867% の FPS 改善を主張 - Impeller 移行: Google Flutter チームとの公開コラボレーション。NImpeller として .NET バインディングは既に公開 されているが、UXDivers のレビューによれば「the full Avalonia rendering backend built on top of these bindings remains in a private internal branch」── すなわち 本体のフルバックエンドは内部実験ブランチ にあり、本記事執筆時点では production-ready ではなく、Avalonia 12 の作業中は一時停止している
- WebAssembly readiness: .NET WASM ツーリングの成熟待ち。MAUI Backend のデモが既にブラウザ上で動作
5.6 Microsoft の姿勢
- 公式の Linux MAUI は「not planned」(MAUI メンテナの David Ortinau 氏が
dotnet/mauiDiscussion #339 で「Linux support in .NET 6 for .NET MAUI is not planned from us」と発言、その後も MAUI 公式 Linux サポートのアナウンスは無し) - そこを Avalonia MAUI Backend が埋める形で、エンジニアレベルの非公式な協業
- MAUI Backend の発表が「with guidance and feedback from engineers in the MAUI ecosystem」という表現に留まっており、UXDivers は「while the Avalonia team has developed this with feedback from engineers in the MAUI ecosystem, this does not constitute official Microsoft endorsement or a first party partnership」── すなわち 公式パートナーシップではない と整理している
6. デメリットと注意点
技術選定型の記事として、率直なデメリットを列挙します。
6.1 モバイルと WebAssembly はまだ「発展途上」
- 公式ブログ自身が「Avalonia は desktop ではプロダクションレディ、mobile では early phase に入る段階」と明言(2022 年の mobile 発表時の表現)
- Avalonia 12 で改善が進んだものの、UXDivers の 2025 年レビューも「primary focus, community, and production track record remain centered on desktop」と明確に述べている
- WebAssembly 上の MAUI Backend デモは「real but rough edges」段階
6.2 エコシステム・サードパーティライブラリ
Qt(25 年超の蓄積)、Flutter(pub.dev の巨大エコシステム)に比べると、Avalonia のサードパーティコントロール・ライブラリのカタログは 明確に小さい。これは商業的に有償化が進んでいる理由でもあります。
6.3 有償化の線引き(Avalonia Pro / 旧 Accelerate)
公式サイト現行表記(avaloniaui.net/accelerate)によれば、現在 Avalonia の構造は以下の通り:
- Free(MIT): コア OSS フレームワーク。Avalonia 12 で WebView もコアに含まれるようになった
- Community: 非商用無料。Visual Studio 拡張、DevTools、Parcel(パッケージング)
- Accelerate(有償、from €89/year): Phase 1 機能として Dev Tools、Media Player、強化版 TreeDataGrid 等。70+ チャートの Charts は coming soon
過去には ホットリロード、Android 向け AOT、ビジュアルデザイナー が有償計画として明示されていました(DEVCLASS 2024 年 12 月報道:「a visual designer for XAML, the XML-based language for defining a UI; hot reload which reflects code changes immediately in a project being debugged, a media player, a TreeDataGrid, an advanced Rich Text control, and ahead-of-time compilation for Android applications」)。コミュニティの懸念もありましたが、最近の整理で「TreeDataGrid 等の核となる旧コントロールはコアに残し、有償版は付加価値ツール群」という方向に再整理されたと公式が説明しています(DEVCLASS 2025-06-26:「the TreeDataGrid has an "enhanced, paid version" in Accelerate」)。
注意点: デザイナー / ホットリロードを使うなら有償 という線引きは現状残っています。コミュニティ版 HotAvalonia という代替もあるが、Avalonia コアチーム自身は「ホットリロードは Accelerate の機能として計画」と明言しています。
6.4 ネイティブ API への直接アクセス
OS の通知、センサー、Bluetooth、システムサービス等への直接アクセスは Xamarin.Essentials / MAUI Essentials のような「.NET Essentials 的バンドル」が Avalonia コアには無く、手動実装が必要(Avalonia.Labs 等のサブプロジェクトはあるが網羅性は限定的)。
6.5 C++ エンジンと接続する場合の追加コスト
LibraryImport / UnmanagedCallersOnly が成熟しても、設計コスト自体が消えるわけではない ことに注意。具体的な落とし穴:
| 項目 | 内容 |
|---|---|
| データ境界のマーシャリング | AST、ネットリスト、グラフのような複雑オブジェクトは境界に出さない。ハンドル + 個別アクセサ API で薄く露出する設計が必要 |
| GC・ピン留め・所有権 | C# から Span<byte> を C++ に渡すなら GC が動かない区間で完結させる。バッファを C++ 側に長期保持させるなら GCHandle.Alloc(Pinned) か unmanaged メモリへコピー |
| コールバックスレッド |
UnmanagedCallersOnly で受けたコールバックは任意の C++ スレッド。Avalonia の UI ツリーを触る前に 必ず Dispatcher.UIThread.Post。これは Qt の QMetaObject::invokeMethod / Qt::QueuedConnection と同じ責務だが、忘れると即クラッシュ or 不可解な再入問題に |
| ビルド・配布 | .NET SDK + C++ ツールチェーン両方が必要。プラットフォームごとの runtime identifier(RID)、.NET 10 の検索パス変更、コード署名・公証(macOS)、Linux の glibc バージョン互換、すべてが複雑化 |
| ABI バージョン管理 | C++ 側ヘッダの変更は C# 側 LibraryImport 宣言と必ず同期。CI でヘッダから C# 宣言を生成するか、互換性レイヤを設けるのが現実解 |
| 境界をまたいだデバッグ | C# デバッガと C++ デバッガを同時にアタッチする必要。Visual Studio の「ネイティブ + マネージド混在デバッグ」は Windows では成熟、macOS/Linux では Rider + lldb 等の組み合わせを工夫する |
6.6 WASM ターゲットの相性
「Avalonia なら WASM 行ける」のは C# 側だけ の話で、C++ エンジンを WASM に持ち込む場合は Emscripten が必要になり、.NET-WASM とネイティブ-WASM の interop は別世界 の制約(Emscripten のリンク、WASI、SharedArrayBuffer/threads 周りの制限、JS 経由のブリッジング)に直面します。
実用的には:
- デスクトップ + モバイルだけが WASM 不要なら問題なし
- ブラウザ展開も視野に入る なら、C++ コアの一部を Rust + wasm-bindgen 等に置き換える、もしくは C++ → Emscripten WASM ビルドを別途用意してブラウザ版だけ別構成、といった割り切りが必要
6.7 実測値 ── NativeAOT の効果(自作ツール SukimaDebug での計測)
ここまで論じてきた「.NET 10 + Avalonia + NativeAOT」の構成が、実際にどの程度効くのか。筆者の自作デバッグツール SukimaDebug で計測した結果を、定量的な裏付けとして示します。
計測環境
- 計測日: 2026年5月20日
- ホスト: WSL2 Ubuntu 24.04 on Windows (AMD Ryzen 9 7950X 16-Core、64 GB RAM)
- カーネル: Linux 6.6.114.1-microsoft-standard-WSL2
- .NET SDK: 10.0.107
- Avalonia: 12.0.3
-
計測ツール:
- 起動時間:
MainWindow.OpenedイベントでDateTime.Now - Process.StartTimeを測定(SUKIMADEBUG_BENCH=1で有効化する instrumentation) - メモリ: GNU
time -f "%M"(=getrusage().ru_maxrss)
- 起動時間:
- 対象: Avalonia + Skia + 自前 ViewModel で構成された通常規模のデスクトップアプリ
- C# コード合計: 11,105 行 / 66 ファイル (src 8,394 + tests 2,711)
- XAML: 788 行 / 2 ファイル
- AOT 出力バイナリ: 23 MB ELF (text=22.5 MB, data=329 KB, bss=7.0 MB)
- AOT 配布フォルダ合計: 37 MB (App + libSkiaSharp.so + libHarfBuzzSharp.so)
サマリ
| 指標 | JIT(dotnet) |
Native AOT | 差分 |
|---|---|---|---|
起動 → MainWindow.Opened(5 回平均) |
886.7 ms | 119.5 ms | −87%(7.4 倍高速) |
| 起動時間の標準偏差 | 約 22 ms | 約 3.9 ms | −82% |
| ピークメモリ(2 秒走行時) | 約 196 MB | 約 148 MB | −24% |
| 配布サイズ | (.NET ランタイム別途) | 37 MB(self-contained) | ― |
| 起動依存 | .NET 10 ランタイム必須 | glibc + libm のみ | ― |
| ビルド時間 | 約 4 秒(Release JIT) | 約 22 秒(clean publish) | +18 秒 |
体感としても「AOT 版の方が立ち上がりが明らかに速い」と分かるレベルの差で、計測値もそれを裏付けています。
計測手法について
起動時間は MainWindow.Opened イベントを Process.StartTime からの差分として計測 しています。これは「カーネルの exec(2) から、Avalonia のメインウィンドウが表示準備完了まで」の所要時間で、プロセスロード、CoreCLR / AOT ランタイム初期化、Avalonia 初期化、XAML パース、MainWindow のコンストラクタ、deferred な DataContext post、最初のレイアウトパスをすべて含みます。env var(SUKIMADEBUG_BENCH=1)で gate されているため、通常実行時は影響ゼロです。
参考までに、当初は「80 MB RSS 到達」を proxy として使っていました(JIT 199 ms / AOT 46 ms = 4.3 倍)。しかしこの proxy は JIT 側を約 4.5 倍過小評価 していました。Avalonia の起動コストは 80 MB 到達後に集中していて、JIT は RSS が 80 MB を超えてからもウィンドウ表示準備に多くの時間を要するためです。実 instrumentation で 7.4 倍まで広がった、というのが正確な姿です。
生データ
起動時間(5 回計測、startup_to_opened_ms):
JIT (Release) Native AOT
Run 1: 886.5 ms Run 1: 114.5 ms
Run 2: 912.8 ms Run 2: 118.1 ms
Run 3: 899.6 ms Run 3: 119.2 ms
Run 4: 853.6 ms Run 4: 125.0 ms
Run 5: 881.1 ms Run 5: 120.5 ms
---- ----
mean: 886.7 ms mean: 119.5 ms
median: 886.5 ms median: 119.2 ms
stddev: ~22 ms stddev: ~3.9 ms
注目すべきは 標準偏差が AOT 側で 1/5 以下(3.9 vs 22 ms)である点です。JIT は cold JIT のばらつきがそのまま起動時間のばらつきになりますが、AOT は 決定論的に動く ため、初回起動時間が予測可能になります。これは UI の体感品質に直結する性質です。
ピークメモリ(3 回計測、getrusage().ru_maxrss):
JIT 1: 195,568 KB (~191 MB) AOT 1: 148,388 KB (~145 MB)
JIT 2: 196,112 KB (~192 MB) AOT 2: 148,300 KB (~145 MB)
JIT 3: 196,104 KB (~192 MB) AOT 3: 148,080 KB (~145 MB)
バイナリ依存(AOT 版):
$ ldd SukimaDebug.App
linux-vdso.so.1
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
/lib64/ld-linux-x86-64.so.2
Avalonia の Skia / HarfBuzz は .so として同梱(libSkiaSharp.so 約 11 MB、libHarfBuzzSharp.so 約 2.7 MB)、本体 ELF が約 23 MB、合計 37 MB の self-contained 一式で配布できます。
なぜ AOT が効くのか(実測の解釈)
886.7 ms → 119.5 ms という差分(約 767 ms)の内訳を分解すると次のとおりです。
- JIT コンパイル自体が消える。JIT 版は起動時、Avalonia の各種コントロール・バインディングのコードを IL バイトコードからネイティブへ変換しながら立ち上がります。Avalonia は内部で多数のリフレクションパスを持つため、cold path の JIT は累積で数百 ms オーダーに達します。AOT はビルド時に変換済みなので、この変換コストがゼロ。767 ms の差分の主因はここ。
- JIT コンパイラ本体が常駐しない。RyuJIT は実行時に約 40 MB のメモリを消費しますが、AOT 版ではこれが完全に不要。メモリ −24% の主因の一つ。
- IL メタデータのロードが不要。JIT 版はアセンブリの IL 部分を必要に応じてロードしますが、AOT 版は IL を捨て、ネイティブコードとリフレクション最小限のメタデータのみを保持。
-
シングルバイナリ化。JIT 版は数十個の DLL を順次ロード(mmap)。AOT 版は 1 つの ELF + Skia / HarfBuzz の 2 つの
.soのみ。ファイル open / page-in の回数が激減し、cold start が劇的に短くなる。 - ビルド時の dead code elimination(trim)。未使用コードが物理的に除去されているため、ワーキングセットと icache プレッシャーが小さい。
- JIT のばらつきの吸収。JIT は実行パスごとにコンパイル順序とインライン化判断が微妙に変わり、cold path のヒット具合で起動時間が ±22 ms ばらつきます。AOT はビルド時に全てが固定されているため、起動が ±3.9 ms に収まる。
推奨ワークフロー
実測を踏まえた現実的な使い分けは:
-
開発時:
dotnet run -c Debug(JIT、デバッガ快適、再実行が約 2 秒) -
配布時:
dotnet publish -c Release -r <rid> -p:PublishAot=true(AOT、起動 7.4 倍 + メモリ −24% + 決定論的) - CI: JIT と AOT の両方をビルドし、AOT 版で smoke test を回す(リフレクション漏れを早期検出)
補足:この測定が示すこと
この数字は「Avalonia 12 + .NET 10 NativeAOT で、デスクトップアプリが現実に publish でき、しかも JIT 版より明確に速く・軽く・予測可能である」という、本記事のアーキテクチャの 実測ベースの証拠 です。3.4 で論じた Compiled Bindings の AOT 適性が、ここで具体的な数字として現れています。
C++ コアと直結する構成では、UI プロセスが軽量・高速・決定論的であるほど、コア計算のスケジューリングやコールバック処理のレイテンシも下がります。「UI 層に重い JIT ウォームアップ時間を払わずに済む」「初回起動時間のばらつきが小さい」 という性質は、C++ エンジンの起動コストと足し合わせて全体起動時間が決まる本記事のアーキテクチャでは、特に効いてくると思います。
7. 適用判断のサマリ
Avalonia + C++ コア構成が向くケース:
- Linux 含むクロスプラットフォームのデスクトップ配信が要件
- IDE 風・密度の高い UI、テーマ統一の必要性
- 既存 C++ 計算カーネル(シミュレータ、CAD、EDA、解析エンジン等)を活かしたい
- .NET の生産性(C#、Roslyn、NuGet、Rider/VS のリファクタリング)を UI 開発で享受したい
- MIT で配布フリーダムが欲しい
避けるべき/別の選択肢を検討すべきケース:
- MCU・ベアメタル・機能安全(ASIL D 等) が必要 → Qt(Qt for MCU、Qt Safe Renderer)
- モバイル(iOS/Android)が主戦場でネイティブな見た目が必須 → MAUI または Flutter
- ブラウザ単独で完結し、C++ コアも WASM 必須 → WebAssembly + Emscripten + (React/Vue/Svelte 等) を検討、Avalonia は補助的
- 既に Qt 商用ライセンスを保有し、長期サポート契約済み → そのまま Qt 継続が合理的
8. まとめ
- .NET の interop は .NET 5 の
UnmanagedCallersOnly、.NET 7 のLibraryImport、.NET 7〜10 の NativeAOT 成熟、.NET 10 の検索パス・IsAotCompatible改善を経て、C++ コアと C# を「狭い C-ABI 境界で対等に」結合する設計が現実的なコストでできる 段階に到達した。Microsoft 自身が C# Dev Kit の C++ Node.js アドオンを .NET 10 NativeAOT で置き換えた事例(2026 年 4 月、Drew Noakes 氏)が、その成熟度を象徴的に示している。 - その UI 層として Avalonia は、Qt のライセンス・XAML 一貫性・ピクセル一貫性の観点で現実的な代替、MAUI と比べて Linux / WASM / デスクトップ志向の観点で優位、JetBrains・Autodesk・Devolutions らの本番採用と Devolutions の 3 年 300 万ドル backing による開発継続性 が揃っている。
- 一方で、モバイル / WASM の experimental 表記、エコシステムの小ささ、デザイナー / ホットリロードの有償化、C++ エンジン接続時の ABI 設計コスト は率直に評価すべきデメリットで、Qt が依然優位な領域(MCU、機能安全、自動車インパネ)も明確に存在する。
- 結論として、「C++ コア + デスクトップ向け .NET UI」を新規に組むなら Avalonia は 2026 年時点でデフォルト候補にして良い。ただし上記のデメリットを自プロジェクトに当てはめて判定すること。
次回以降では、本記事で示したアーキテクチャ (Avalonia ホスト + C++ コア + LibraryImport / UnmanagedCallersOnly 境界)の具体実装例 (ハンドルの所有権、エラーハンドリング、シングルファイル配布、.NET 10 検索パス問題の対応) などを扱ってみたいと思っています。