C++やRust以外で Node.js の native addon を本気で書けるのか。結論から言うと、2026年4月20日に Microsoft が公開した .NET Native AOT × Node.js interop の事例で、答えは「用途を選べば yes」に変わりました。Microsoft は C# Dev Kit 向けに、C++ 製 Node.js addon を C# + Native AOT で置き換える方法を公開し、文字列マーシャリングや Windows Registry アクセスのような処理では C++ 実装と比べて実務上ほぼ差がないと説明しています。(Microsoft for Developers)
ただし、.NET Native AOT が Node.js ネイティブ拡張開発の万能解になったわけではありません。強いのは、Node-API 境界を薄く保ち、.NET 側でまとまった仕事を完結させる設計です。逆に、細かい JS/native の往復、複雑な JS オブジェクトや callback のやり取り、Buffer を使った zero-copy 最適化が主戦場のモジュールでは、設計難度は今も高いままです。この記事では、.NET Native AOT が「本命候補」になる条件と、まだ慎重に見るべき条件を実装レベルで整理します。(Microsoft for Developers)
.NET Native AOT と Node.js interop の2026年最新動向
今回の更新が重要なのは、机上の PoC ではなく、Microsoft 自身が VS Code 拡張の実運用背景からこの方法を紹介した点です。元の C++ addon は node-gyp でビルドされ、チームは Python 依存を contributor setup の負担として挙げていました。Node.js 公式 docs でも node-gyp は Python を必要とするとされており、Microsoft の話は「たまたま自社事情にだけ効く裏技」ではなく、Node native addon 開発でよくある摩擦に正面から当たっています。(Microsoft for Developers)
Microsoft が示した実装の骨格は、Node-API に必要な最小限の C ABI を C# から直接組み立てる、というものです。ポイントは次の4つです。(Microsoft for Developers)
- Node.js が期待する
napi_register_module_v1を、[UnmanagedCallersOnly]付きメソッドとして公開する。Native AOT はEntryPointを持つUnmanagedCallersOnlyメソッドを公開 C エントリポイントにできます。(Microsoft for Developers) - Node-API 関数は
LibraryImport("node")で宣言し、NativeLibrary.SetDllImportResolverとNativeLibrary.GetMainProgramHandle()でホストの Node プロセスから解決する。Microsoft のサンプルはこの方法でnapi_create_string_utf8などを C# から直接呼んでいます。(Microsoft for Developers) dotnet publishで OS ごとの共有ライブラリを作り、それを.nodeにリネームしてrequire()で読み込む。Node.js は.nodeを動的リンクライブラリとして初期化します。(Microsoft for Developers)- サンプルのプロジェクトは
net10.0、PublishAot=true、AllowUnsafeBlocks=trueという最小構成で成立しています。つまり発想としては大げさでも、実装は意外に薄くできます。(Microsoft for Developers)
.NET Native AOT で Node.js native addon が成立する技術的な理由
Node-API は C++ 専用ではない
Node-API は Node.js 本体がメンテナンスする stable な C API で、JavaScript エンジン実装から独立し、ABI 互換性を保つことを目的にしています。Node.js docs でも「他言語の addon を Node-API の上に書ける」と明記されており、Node major version をまたいで再コンパイルなしで動かせるのは大きな利点です。逆に ABI 互換を保ちたいなら、V8 や libuv ではなく Node-API に寄せる必要があります。つまり、障壁は「C++で書くこと」ではなく「C ABI を正しく実装すること」です。(Node.js)
.NET 側も「読み込まれる共有ライブラリ」になれる
Native AOT は、.NET class library を self-contained な shared library として publish でき、UnmanagedCallersOnly 付きメソッドを公開エントリポイントとして外へ出せます。しかも生成物は .NET runtime の別インストールを必要としません。Node.js addon が求めるのも、まさに「共有ライブラリとして読み込めること」と「期待したシンボルを公開すること」なので、両者の噛み合わせはかなり素直です。(Microsoft Learn)
ただし、ここには Native AOT 固有の前提もあります。Native AOT では dynamic loading や System.Reflection.Emit を使えず、trimming が必須で、Windows では built-in COM もありません。さらに shared library は作れても、dlclose や FreeLibrary 相当の unload はサポートされていません。Node.js addon として「読み込む」ことはできますが、普段の JIT .NET と同じ自由度があるわけではありません。(Microsoft Learn)
interop 層も AOT 向きに揃ってきた
今回の実装が現実味を持つ理由は、公開エントリポイントだけではありません。Microsoft の sample は LibraryImport を使って Node-API を呼び出しており、.NET の interop guidance でも .NET 7 以降は LibraryImport が推奨されています。P/Invoke source generation はマーシャリングコードをコンパイル時に生成するため、runtime-generated IL stub に依存しません。AOT と trimming を前提にした Node-API の薄いラッパーには、かなり相性がいい組み合わせです。(Microsoft for Developers)
さらに 2026年時点の .NET 10 では、IsAotCompatible や VerifyReferenceAotCompatibility を使って依存ライブラリの AOT 適合性を前より判断しやすくなっています。Node.js addon 本体が薄くても、背後で呼ぶ .NET ライブラリが AOT 非対応なら実戦投入は難しいので、この改善は地味に大きいです。(Microsoft Learn)
.NET Native AOT の性能はどこで効くのか
速くなるのは「境界の内側に仕事を寄せた」とき
Native AOT そのものには、通常の .NET publish と比べて startup time の短縮、小さめの memory footprint、runtime 不要の自己完結配布という利点があります。そこに加えて Microsoft は、今回の registry addon では C++ 実装と比べて実用上 meaningful な差はなかったと述べています。さらに Microsoft 自身が、今は別プロセス + pipe で動かしている .NET 処理を、将来的には Node プロセス内に寄せて serialization と process management の overhead を減らせる可能性に言及しています。Native AOT の本当の価値は、「C# が C++ より速い」ではなく、「境界の外に出していた .NET 処理を in-process に戻せる」点にあります。(Microsoft Learn)
遅くなりやすいのは「細かい往復」
一方で、Node-API 境界をまたぐコストが消えるわけではありません。Microsoft の sample でも、引数取り出しは napi_get_cb_info と UTF-8 文字列変換、戻り値は napi_create_string_utf8 で明示的に行い、Span<T>、stackalloc、ArrayPool を使って通常サイズの文字列で heap allocation を減らす工夫をしています。ここから分かるのは、性能を出す鍵は「Native AOT だから自動的に速い」ではなく、「1回の呼び出しで意味のある仕事を終わらせる」API 設計だということです。(Microsoft for Developers)
Buffer/TypedArray 中心の高速化は「可能」だが設計難度が上がる
Node-API には external ArrayBuffer、external Buffer、TypedArray、napi_get_buffer_info などの API があり、zero-copy 寄りの設計自体は可能です。ただし external buffer は finalize callback を伴い、外部メモリの寿命を addon 側で保証する必要があります。さらに .NET 側では [UnmanagedCallersOnly] の公開メソッドに blittable な引数しか置けず、pinning や GCHandle 管理も自前で考える必要があります。つまり、Buffer を軸にしたかなり低レベルなモジュールほど、「書けるか」より「安全に維持できるか」が勝負になります。(Node.js)
ここで見落としやすいのが runtime 差です。Node.js docs では、external buffer 系 API は Node.js 以外の一部 runtime では使えない場合があり、Electron では napi_no_external_buffers_allowed が返る可能性があると案内されています。VS Code 拡張や Electron アプリまで射程に入るなら、zero-copy 前提の設計は早めに検証した方が安全です。(Node.js)
async 設計は最初から入れる
CPU-heavy な処理や blocking I/O を同期 addon としてそのまま呼ぶと、Node.js 側の main thread を止めます。Node-API には napi_create_async_work があり、execute callback は worker pool thread、complete callback は main event loop thread で実行されます。さらに独自スレッドを使う場合、JavaScript 関数は原則 main thread からしか呼べず、napi_env や napi_value を必要とする Node-API を別 thread から直接触ってはいけません。その橋渡しのために napi_threadsafe_function が用意されています。高性能を狙うなら、Native AOT に期待する前に async 設計を入れるべきです。(Node.js)
Node.js ネイティブ拡張で本命候補になりやすい案件
向いているケース
- OS 固有 API やプラットフォーム機能の橋渡し。Microsoft の例そのものが Windows Registry で、Node.js に組み込みのない機能を .NET で安全に包んで expose する使い方は相性がいいです。(Microsoft for Developers)
- 既存の .NET ロジックを別プロセスではなく Node プロセス内で動かしたい案件。pipe 越しの serialization や process lifecycle 管理を減らせる余地があり、Microsoft もここを今後の可能性として挙げています。(Microsoft for Developers)
- 内部ツール、企業内 CLI、VS Code 拡張のように、配布先よりも開発体験と保守性の価値が高い案件。Microsoft は Python 依存を外し、CI と contributor setup を軽くできた点を実益として挙げています。(Microsoft for Developers)
- API を粗い粒度で設計できる案件。1回の呼び出しで「取得」「解析」「変換」「検証」をある程度まとめて終わらせるほど、Node-API 境界のコストを吸収しやすくなります。これは Microsoft の sample があえて薄い Node-API 層 + まとまった .NET 処理、という構図で成立していることからも読み取れます。(Microsoft for Developers)
慎重に見るべきケース
- Worker threads や Electron のように、同じ addon が複数 context / 複数 thread / 複数環境で初期化されうる案件です。Node.js docs は、addon は複数 context で同時にロードされうるため、global static data を雑に持たず、環境ごとの state を分ける必要があると説明しています。(Node.js)
- AOT 非対応依存が多い案件です。Native AOT では dynamic loading、runtime code generation、
System.Reflection.Emitが使えず、System.Linq.Expressionsも interpreted form になります。普段の .NET サービスで問題なかったライブラリが、そのまま addon 化できるとは限りません。(Microsoft Learn) - JavaScript class/object/callback を深くやり取りする案件です。Microsoft の記事でも、entire .NET classes の公開や callback まで扱うなら higher-level framework を検討する価値があると触れています。薄い Native AOT + 生 Node-API だけで押し切ると、保守コストが急に上がりやすい領域です。(Microsoft for Developers)
- 公開 npm package として幅広い OS/arch に配る案件です。Node.js native addon はもともと precompiled binary 配布が一般的で、Native AOT 側も runtime identifier ごとの publish が前提です。ビルドマトリクスと配布設計を嫌うなら、最初から辛くなります。(Node.js)
- 配布サイズが重要な案件です。Native AOT は required runtime libraries を含み、generic の事前特殊化が disk size に効くことがあります。throughput だけでなく npm tarball size も見るべきです。(Microsoft Learn)
採用前に確認したい制約と落とし穴
napi_env をグローバルに握らない
Node-API docs は、napi_env を一般用途でキャッシュしたり、異なる Worker thread 間で回したりしてはいけないと明記しています。addon instance が unload されれば napi_env は無効になり、環境ごとの data は napi_set_instance_data / napi_get_instance_data で管理するのが基本です。stateful な .NET addon を書くなら、static フィールドより「env 単位の state」に寄せる方が安全です。(Node.js)
[UnmanagedCallersOnly] の例外をそのまま外へ出さない
Microsoft の sample でも強調されている通り、[UnmanagedCallersOnly] メソッドで unhandled exception が起きると host process が落ちます。Node.js addon では「クラッシュ = そのまま Node プロセス死亡」なので、公開関数は必ず try/catch し、JavaScript 側の Error に変換する設計にしておくべきです。(Microsoft for Developers)
開発体験は改善しても、native toolchain は消えない
.NET Native AOT なら C++ toolchain から完全解放される と考えるのは危険です。Node.js docs は native addon 開発・配布に追加 toolchain が必要だと説明しており、Microsoft も自分たちの改善点を「Python 依存が消えたこと」と表現していますが、yarn install に必要なのは Node.js、C++ tooling、.NET SDK です。Native AOT 自体も Windows では Visual Studio の Desktop development with C++、Linux では clang 系ツールチェーン、macOS では Xcode command line tools を前提にします。(Node.js)
ローダーはまず require() 前提で組む
Microsoft の sample は require() で .node を読み込んでいます。Node.js docs でも binary addon の import は --experimental-addon-modules 前提で Stability 1.0 / Early development です。さらに require('addon') は、同名の addon.js があれば addon.node よりそちらを優先します。最初は require() + 明示パスで確実に動かし、必要なら ESM から薄い JS wrapper で包むのが堅実です。(Microsoft for Developers)
Linux の publish は「最小サポート環境」で作る
Node ecosystem で public 配布を考えるなら、Linux バイナリの作り方も重要です。.NET docs は、Ubuntu 20.04 で作った Native AOT binary は 20.04 以降で動くが 18.04 では動かないと説明しています。つまり linux-x64 を1本作れば十分、ではなく、どの distro baseline を切るかまで設計する必要があります。公開配布なら oldest supported distro でビルドし、Node 側の precompiled binary 配布パターンに合わせて運ぶ方が安全です。(Microsoft Learn)
デバッグ・運用面も PoC で見る
Native AOT publish では debug information が Windows なら .pdb、Linux なら .dbg、macOS なら .dSYM として別出力され、diagnostics や profiling にも制約があります。さらに Native AOT shared library は unload をサポートしません。性能だけ見て採用を決めると、クラッシュ解析や再読み込みを含む運用で後から痛みが出やすいので、PoC では処理性能だけでなくデバッグしやすさも確認しておくべきです。(Microsoft Learn)
結論
2026年4月時点で、.NET Native AOT は Node.js ネイティブ拡張開発に対する本命候補です。ただし本命になるのは、「Node-API にだけ触れる薄い境界」「AOT-compatible な .NET 依存」「1回の呼び出しでまとまった仕事をする API 設計」「OS/arch ごとの配布を受け入れる運用」の4条件が揃う場合に限られます。Microsoft の事例が価値を持つのは、まさにその条件で C++ addon を C# に置き換え、実務上の性能と開発体験の両方で筋が通ると示したからです。(Microsoft for Developers)
次にやるべきことは、いきなり大きな addon を移植することではありません。まずは「高価値だが境界を粗く切れる 1 API」だけを選び、net10.0 の Native AOT class library として .node 化し、同期版と async 版の両方で起動時間、処理性能、配布サイズ、クラッシュ時の扱いを測ってください。その1本で良い結果が出るなら、.NET Native AOT と Node.js interop は、C++やRustしかないと思われがちな領域に現実的な第3の選択肢として入ってきます。(Microsoft for Developers)

コメント