理系学生日記

おまえはいつまで学生気分なのか

Electron から native module を使うときの NODE_MODULE_VERSION のエラー

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 の方が先行するはず)。 結果として、

  1. ローカル PC 上で native module を (gyp で) ビルドする際は Node.js の V8 がターゲットとされ
  2. それを動かす 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 から使用できるようになりました。