Electron でとある node のモジュールを使おうと思ったら
(snip) hoge.node' was compiled against a different Node.js version using NODE_MODULE_VERSION 45. This version of Node.js requires NODE_MODULE_VERSION 64. Please try re-compiling or re-installing the module (for instance, using `npm rebuild` or `npm install`).
というエラーメッセージが出力されてしまったというときの戦いの記録です。
問題の分析
NODE_MODULE_VERSION とは
ここで言う NODE_MODULE_VERSION
とは何か。これは、Node.js における ABI の version を指します。
つまりは、Node.js のリリースのうち、バイナリレベルの互換性があるものがどれかを識別するための version ですね。
NODE_MODULE_VERSIONは、Node.jsのABI(アプリケーションバイナリインタフェース)のバージョン番号を指します。このバージョンは、再コンパイルすることなくC++アドオンのバイナリーをロード可能か確認するために使われます。 リリース一覧 | Node.js
以上から、最初に述べた NODE_MODULE_VERSION 云々のエラーは、コンパイルした module と Node.js のランタイムとの間で ABI に齟齬が出てしまっている、というメッセージであることが分かります。
Native Node Module
なんで JavaScript において ABI が関係するのか、というと、当該のモジュールがいわゆる Native Node Module と呼ばれるモジュールであるためでした。
Native Node Module とは何かというと C++ 等で書かれているモジュールであって、Node.js から呼びだせるものになります。 詳しくは C++ addons | Node.js v17.6.0 Documentation あたりを見て頂ければと思いますが、
- C++ で元々書かれているライブラリを Node.js から使いたい
- パフォーマンスクリティカルな機能を実装するので特定の箇所だけ C++ で書きたい。あとは JS で書きたい。
というような要求を叶えるものになります。 例えば grpc - npm なんかは、まさに native module の 1 つになります。 (ちなみに、今回問題になったのは grpc ではありません)
native module に関しては、単純に C++ のコンパイラでコンパイルすれば良いというわけではなく、Node.js から読み込める形式の .node
形式のモジュールとしてビルドしなければなりません。Node.js においてはこのために GYP - Generate Your Projects. を使うことになっており、簡単に build するためのツールとして GitHub - nodejs/node-gyp: Node.js native addon build tool が提供されています。
この node-gyp
を使えば、簡単に native module が作れると。
native module については、当該のモジュールのインストールプロセス ({npm,yarn} install
) の中で自動的に node-gyp
を使ってコンパイルしてくれるようになっていて、ターゲットとなる Node.js の ABI もその中で定まります。しかし、それでもモジュールとランタイムの間で ABI に齟齬が出るのはなぜなのか。
Electron のアーキテクチャ
今回、Native Module を使いたかったのは Electron 上で動作するアプリです。Electron は Node.js と Chromium を内蔵するアーキテクチャを取りますが、そのいずれもが Chromium 側の V8 を使うことになっています。
In Electron, Node.js and Chromium share a single V8 instance—usually the version that Chromium is using. Most of the time this just works but sometimes it means patching Node.js. https://electronjs.org/docs/tutorial/about#updating-dependencies
ここでの V8 は、一般に(ローカル PC 上の) Node.js の V8 とは version が異なります (普通、Chromium の V8 version の方が先行するはず)。 結果として、
- ローカル PC 上で native module を (gyp で) ビルドする際は Node.js の V8 がターゲットとされ
- それを動かす Electron では別の V8 が使用され
Module と runtime の間で ABI が一致しなくなる状況が発生した、ということのようです。
解決策
問題が分析できたら解決策は明らかであり、それは「native module を electron の V8 をターゲットとしてコンパイルする」ことです。 そして、実はそのものズバリのページが electron の公式に存在します。
いくつか目的を果たすための方法が紹介されていますが、今回選んだのは electron-rebuild でした。
electron-rebuild は、使用している electron の version を自動的に判断し、それを target ととして node_modules
ディレクトリ配下の native module をコンパイルしてくれるという便利ツールです。
これをインストールした後に $(npm bin)/electron-rebuild
をかければ最初のエラーメッセージが表示されなくなり、無事に目的の native module が electron から使用できるようになりました。