メインコンテンツへ飛ぶ

"内部詳細" タグの記事が 1 件の投稿 件あります

全てのタグを表示

WebView2 と Electron

· 読むのにかかる時間 1 分

この数週間で、新しい WebView2 と Electron の違いについていくつかご質問をいただきました。

両チームとも、ウェブ技術をデスクトップ上で最高のものにするという目標を掲げているため、共通点の総合的な比較を検討してみましょう。

Electron と WebView2 は、行動が早く、常に進化しているプロジェクトです。 現在の Electron と WebView2 の共通点と相違点を簡単にまとめてみました。


アーキテクチャの概要

Electron と WebView2 はどちらも、ウェブコンテンツのレンダリングに Chromium ソースを使用しています。 厳密には WebView2 は Edge のソースからビルドされています。しかし、Edge は Chromium のソースをフォークしてビルドされています。 Electron は Chrome と DLL を共有していません。 WebView2 のバイナリは、Edge (Edge 90 の安定チャンネル) をハードリンクしているので、ディスクや一部の動作セットを共有しています。 詳細は Evergreen ディストリビューションモード をご参照ください。

Electron アプリは、常に開発時のバージョンの Electron をバンドルして頒布しています。 WebView2 では、頒布にあたって 2 つの選択肢があります。 アプリケーションが開発された WebView2 ライブラリをそのままバンドルすることもできますし、システム上に既存の共有ランタイム版を使用することもできます。 WebView2 は、共有ランタイムが見つからない場合のブートストラップインストーラーを含む、各手段向けのツールを提供しています。 WebView2 は、Windows 11 から 標準で 付属します。

フレームワークをバンドルしているアプリケーションは、マイナーなセキュリティリリースを含め、そのフレームワークをアップデートする責任があります。 共有 WebView2 ランタイムを使用しているアプリの場合、WebView2 には Chrome や Edge に似た独自の更新機能が用意されており、アプリとは独立して実行されます。 Electron と同じく、アプリケーションのコードやその他の依存関係の更新は開発者の責任です。 Electron も WebView2 も Windows Update では管理されません。

Electron と WebView2 は、どちらも Chromium のマルチプロセスアーキテクチャを継承しています。つまり、1 つのメインプロセスが 1 つ以上のレンダラープロセスと通信します。 これらのプロセスは、システム上で動作している他のアプリケーションと完全に分離されます。 すべての Electron アプリケーションは、ルートのブラウザプロセス、いくつかのユーティリティプロセス、0 個以上のレンダープロセスを含む、独立したプロセスツリーを構成します。 同じ ユーザーデータフォルダ を使用している WebView2 アプリ (スイートアプリのようなもの) は、レンダラープロセス以外を共有します。 異なるデータフォルダを使用している WebView2 アプリは、プロセスを共有しません。

  • ElectronJS プロセスモデル:

    ElectronJS プロセスモデル図

  • WebView2 ベースのアプリケーションプロセスモデル:

    WebView2 プロセスモデル図

WebView2 のプロセスモデルElectron のプロセスモデル についてはこちらをご覧ください。

Electron は、メニュー、ファイルシステムへのアクセス、通知など、デスクトップアプリケーションの一般的需要に応える API を提供します。 WebView2 は、WinForms、WPF、WinUI、Win32 などのアプリケーションフレームワークに統合されることを目的としたコンポーネントです。 WebView2 は JavaScript によるウェブ規格外の OS API を提供しません。

Electron は Node.js を統合しています。 Electron アプリケーションは、レンダラープロセスやメインプロセスから Node.js API、モジュール、Node ネイティブアドオンを利用できます。 WebView2 アプリケーションは、アプリケーションの他の部分が書かれている言語やフレームワークを前提にしていません。 JavaScript コードからオペレーティングシステムへアクセスするには、アプリケーションホストプロセスを介する必要があります。

Electron は、Fugu Project が開発した API を含むウェブ API との互換性を維持するよう努めています。 こちらに Electron の Fugu API 対応状況のスナップショット を用意しました。 WebView2 では、Edge との API の違い について同様のリストを作成しています。

Electron でのウェブコンテンツのセキュリティモデルは、フルアクセスからフルサンドボックスまで設定可能です。 WebView2 のコンテンツは常にサンドボックス化されます。 Electron はセキュリティモデルの選択について、包括的なセキュリティドキュメント を用意しています。 WebView2 にも セキュリティのベストプラクティス が用意されています。

Electron のソースは GitHub 上でメンテンスされており、自由に利用できます。 アプリケーションは、Electron の独自 ブランド を構築できるように変更を加えられます。 WebView2 のソースは GitHub 上で利用できません。

簡単な概要:

ElectronWebView2
ビルドの依存関係Chromiumエッジ
GitHub 上でコードが利用可能ありなし
Edge/Chrome DLL の共有なしあり (Edge 90 のもの)
アプリケーション間でのランタイム共有なし任意
アプリケーション APIありなし
Node.jsありなし
サンドボックス任意常時
アプリケーションフレームワークの必要性なしあり
サポートされているプラットフォームMac, Win, LinuxWin (Mac/Linux は計画中)
アプリ間でのプロセス共有なし任意
フレームワークの更新機構アプリケーションWebView2

パフォーマンスの議論

ウェブコンテンツのレンダリングに関しては、Electron、WebView2、その他 Chromium ベースのレンダラーの間におけるパフォーマンスの差はほとんどないと考えています。 私たちは、潜在的なパフォーマンスの違いを調査するご興味のある方向けに Electron、C++ + WebView2、C# + WebView2 で構築したアプリの土台 を作成しました。

ウェブコンテンツのレンダリング 以外 にもいくつかの違いがあり、Electron、WebView2、Edge などの関係者は、PWA を含めた詳細な比較を行うことに興味を示しています。

プロセス間通信 (IPC)

プロセス間通信は、Electron アプリでのパフォーマンスを考慮する必要があるでしょう。これにはすぐに強調すべき違いがあります。

Chromium では、サンドボックス化したレンダラーとシステムの他の部分との間で、ブラウザプロセスが IPC ブローカーとして機能します。 Electron ではサンドボックスのないレンダープロセスにできますが、多くのアプリはセキュリティ強化のためにサンドボックスを有効にしています。 WebView2 は常にサンドボックスが有効なので、ほとんどの Electron および WebView2 アプリでは IPC が全体のパフォーマンスに影響を与えます。

Electron と WebView2 はプロセスモデルが似ていますが、基礎の IPC が異なります。 JavaScript と C++ や C# の間で通信するには、マーシャリング が必要です。最も一般的なのは JSON 文字列への変換でしょう。 JSON のシリアライズ/パースは重い処理であり、この IPC のボトルネックはパフォーマンスに悪影響を及ぼします。 Edge 93 以降、WV2 はネットワークイベントに CBOR を使用します。

Electron は MessagePorts API を介した直接の IPC を任意の 2 つのプロセス間でサポートしており、これは 構造化複製アルゴリズム を利用しています。 これを活用するアプリケーションは、プロセス間でオブジェクトを送信する際の JSON シリアライズのコストを回避できます。

概要

Electron と WebView2 にはいくつかの違いがありますが、ウェブコンテンツのレンダリング方法に関しては大きな違いはありません。 最終的には、アプリケーションのアーキテクチャと JavaScript ライブラリ/フレームワークが、メモリとパフォーマンスに何よりも大きな影響を与えます。なぜなら、実行箇所に関わらず Chromium は Chromium だからです。

この記事をレビューしてくださり、WebView2 アーキテクチャの最新情報を提供して頂いた WebView2 チームに感謝します。 WebView2 チームの皆さんは プロジェクトのフィードバック を歓迎しています。

Electron でネイティブから JavaScript へ

· 読むのにかかる時間 1 分

C++ や Objective-C で記述された Electron の機能は、どのように JavaScript となってエンドユーザーが利用できるのでしょうか。


背景

Electron は、開発者の参入障壁を取り下げることを主目的とした JavaScript プラットフォームで、プラットフォーム固有の実装を気にせずに堅牢なデスクトップアプリを構築できます。 ただし、Electron 自身の中核には、特定システムの言語で記述するようなプラットフォーム固有の機能も必要です。

実際には Electron がネイティブコードを扱うので、単一の JavaScript API に集中できます。

一体、どのように動作しているのでしょうか。 C++ や Objective-C で記述された Electron の機能は、どのように JavaScript となってエンドユーザーが利用できるのでしょうか。

この道筋を追いかけるために、app モジュール から始めましょう。

lib/ ディレクトリ内の app.ts ファイルを開くと、その上部に以下のようなコードの行があります。

const binding = process.electronBinding('app');

この行は、開発者が使用している C++/Objective-C モジュールを JavaScript にバインドする Electron の仕組みをまさに表しています。 この関数は ElectronBindings クラスの、ヘッダーと 実装ファイル によって作成されます。

process.electronBinding

これらのファイルは Node.js の process.binding のように動作する process.electronBinding 関数を追加します。 process.binding は Node.js の require() メソッドよりローレベルの実装です。ただし、他の JS で書かれたコードではなくネイティブコードを require することができます。 このカスタム process.electronBinding 関数は Electron からネイティブコードをロードする機能を与えます。

トップレベルの JavaScript モジュール (app など) がこのネイティブコードを require する場合、そのネイティブコードの状態はどのように決定および設定されるのでしょうか。 そのメソッドはどこまで JavaScript に公開されるのでしょうか。 プロパティではどうなのでしょうか。

native_mate

現時点では、この疑問には native_mate が答えてくれます。これは、C++ と JavaScript の間で型をマーシャリングしやすくする Chromium の gin ライブラリ のフォークです。

native_mate/native_mate の中には、object_template_builder のヘッダーと実装ファイルがあります。 これにより、JavaScript 開発者が望むように適合する形式のネイティブコードでモジュールを形成します。

mate::ObjectTemplateBuilder

すべての Electron モジュールを object として見ると、object_template_builder で構築する理由がわかりやすくなります。 このクラスは、C++ で記述された Google によるオープンソースで高性能の JavaScript および WebAssembly エンジン、V8 が公開するクラスの上に構築されます。 V8 は JavaScript (ECMAScript) の仕様を実装しているため、ネイティブ機能の実装を JavaScript の実装に直接関連付けることができます。 たとえば、v8::ObjectTemplate は専用のコンストラクタ関数とプロトタイプなしで JavaScript オブジェクトを提供します。 Object[.prototype] を使用するため、JavaScript での Object.create() と等価です。

この動作確認は、アプリモジュールの実装ファイル atom_api_app.cc を参照してください。 下部には以下のようなものがあります。

mate::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
.SetMethod("getGPUInfo", &App::GetGPUInfo)

上記の行では、.SetMethodmate::ObjectTemplateBuilder で呼び出されます。 .SetMethodObjectTemplateBuilder クラスの任意のインスタンスで呼び出し、以下の構文で JavaScript の Object プロトタイプ にメソッドを設定できます。

.SetMethod("method_name", &function_to_bind)

これは以下の JavaScript と等価です。

function App{}
App.prototype.getGPUInfo = function () {
// ここに実装
}

このクラスには以下のようなモジュールにプロパティをセットする関数も含まれます。

.SetProperty("property_name", &getter_function_to_bind)

または

.SetProperty("property_name", &getter_function_to_bind, &setter_function_to_bind)

これらは、以下のような Object.defineProperty による JavaScript 実装になります。

function App {}
Object.defineProperty(App.prototype, 'myProperty', {
get() {
return _myProperty
}
})

aND

function App {}
Object.defineProperty(App.prototype, 'myProperty', {
get() {
return _myProperty
}
set(newPropertyValue) {
_myProperty = newPropertyValue
}
})

これによって開発者が予期するようなプロトタイプとプロパティで形成された JavaScript オブジェクトを作成することができ、よりローレベルのシステムで実装された関数とプロパティでもよりはっきりと推論します!

特定のモジュールメソッドの実装場所に関する決定は、それ自体が複雑かつ多くの場合に置いて非決定的です。これについては今後の記事で補います。

Electron の舞台裏: Chromium をライブラリとしてビルドする

· 読むのにかかる時間 1 分

Electron は、Google のオープンソースプロジェクト Chromium をベースにしています。このプロジェクトは、他のプロジェクトで使用することを想定していません。 この記事では、Electron のライブラリとしての Chromium がどのように構築されているのか、またビルドシステムがどのように進化してきたのかを紹介します。


CEF の利用

Chromium Embedded Framework (CEF) は、Chromium をライブラリ化し、Chromium のコードベースに基づいて安定した API を提供するプロジェクトです。 黎明期の Atom エディタや NW.js では CEF を使用していました。

安定した API を維持するために、CEF は Chromium の詳細をすべて隠蔽し、独自のインターフェースで Chromium の API をラップします。 そのため、Node.js をウェブページに統合するような、内部の Chromium API にアクセスする必要があったとき、CEF の利点が障害になりました。

そのため、結局 Electron も NW.js も Chromium の API を直接使うように切り替えました。

Chromium を部品としてビルドする

Chromium は公式には外部プロジェクトをサポートしていませんが、コードベースはモジュール化されており、Chromium をベースに小さなブラウザを簡単に構築できます。 ブラウザインターフェイスを提供するコアモジュールは、コンテンツモジュールと呼ばれています。

コンテンツモジュールでプロジェクトを開発する際は、Chromium をプロジェクトの部品としてビルドするのが一番簡単です。 このためには、まず Chromium のソースコードをチェックアウトしてから、プロジェクトを Chromium の DEPS ファイルに追加します。

NW.js や Electron の非常に初期のバージョンでは、このようなビルド方法を使用しています。

この欠点は、Chromium がとても大きいコードベースであるため、相応に強力な マシンでビルドする必要があるということです。 一般的なラップトップであれば、5 時間以上かかります。 そのため、貢献できる開発者の数に多大な影響を与え、開発も遅くなりかねません。

単一の共有ライブラリとして Chromium をビルドする

コンテンツモジュールのユーザからすれば、ほとんどの場合で Electron が Chromium のコードを修正する必要はないので、Electron のビルドを改善する明白な方法は Chromium を共有ライブラリとしてビルドして Electron 内でそれをリンクすることです。 これにより、開発者は Electron に貢献する際に Chromium すべてをビルドする必要がなくなります。

libchromiumcontent プロジェクトは @aroben によってこのために作成されました。 これは、Chromium のコンテンツモジュールを共有ライブラリとしてビルドし、Chromium のヘッダとビルド済みバイナリをダウンロードできるようにします。 libchromiumcontent の初期バージョンのコードは こちらのリンクに あります。

libchromiumcontent の一部として brightray プロジェクトも生まれました。これはコンテンツモジュールの周りに薄い層を提供します。

libchromiumcontent と brightray を併用することで、開発者は Chromium のビルドの詳細に踏み込まずに素早くブラウザをビルドできます。 そして、プロジェクトをビルドするための高速なネットワークと強力なマシンが必要なくなります。

Electron 以外に、Breach ブラウザ のように、この方法でビルドされた Chromium をベースにしたプロジェクトもあります。

エクスポートしたシンボルのフィルタリング

Windows では、単一の共有ライブラリでエクスポートできるシンボル数に制限があります。 Chromium のコードベースが大きくなるにつれ、libchromiumcontent がエクスポートするシンボル数はすぐに制限を超えてしまいました。

これは、DLL ファイル生成時に不要なシンボルをフィルタリングすることで解決しました。 リンカに .def ファイルを供給し、スクリプトで 名前空間の配下のシンボルをエクスポートすべきかどうか判断 することで動作させました。

このアプローチを取ることで、Chromium がエクスポートされたシンボルを新しく追加し続けても、libchromiumcontent はより多くシンボルを削除することで共有ライブラリファイルを生成できるようになりました。

コンポーネントビルド

libchromiumcontent の次の段階の話をする前に、まずは Chromium におけるコンポーネントビルドの概念を紹介しておきます。

巨大プロジェクトであるため、Chromium ではビルド時のリンクの段階で非常に長い時間がかかってしまいます。 大抵、開発者がちょっとした変更を加えると、最終的な出力が得られるまで 10 分ほどかかります。 これを解決するため、Chromium ではコンポーネントビルドを導入しました。Chromium 内の各モジュールを分離された共有ライブラリとしてビルドすることで、最終的なリンク作業に費やす時間が気にならないようにしました。

生バイナリの頒布

Chromium が成長を続ける中で、Chromium のエクスポートされたシンボルがあまりにも多くなり、コンテンツモジュールや Webkit のシンボルですらも制限を超えるようになりました。 シンボルを削減するだけでは、使用可能な共有ライブラリを生成できなくなったのです。

最終的に、単一の共有ライブラリを生成する代わりに Chromium の生バイナリを頒布 しなければなりませんでした。

先ほど紹介したように、Chromium には 2 つのビルドモードがあります。 生のバイナリを頒布した結果、libchromiumcontent ではバイナリに対して 2 種類のディストリビューションを頒布しなければならなくなりました。 1 つは static_library ビルドと呼ばれるもので、Chromium の通常ビルドで生成された各モジュールすべての静的ライブラリをインクルードします。 もう 1 つは shared_library で、コンポーネントビルドで生成された各モジュールの共有ライブラリすべてをインクルードします。

Electron では、デバッグ版を libchromiumcontent の shared_library 版とリンクしています。これは、ダウンロードが少なく、最終的な実行ファイルをリンクする際に時間がかからないためです。 また、リリース版の Electron は libchromiumcontent の static_library 版とリンクしています。コンパイラはデバッグに重要なシンボルを完全に生成でき、リンカはどのオブジェクトファイルが必要かそうでないかを知っているため、より良い最適化を行えます。

そのため、通常の開発において開発者はデバッグ版をビルドするだけでよく、良好なネットワークや強力なマシンは不要です。 リリース版では、ビルドにより良いハードウェアを必要としますが、より最適化されたバイナリを生成できます。

gn の更新

世界的に見ても最大級のプロジェクトであるため、通常のビルドシステムはほとんど Chromium のビルドには適していません。Chromium チームは独自のビルドツールを開発しています。

初期バージョンの Chromium はビルドシステムとして gyp を使用していましたが、動作が遅く、複雑なプロジェクトでは設定ファイルがわかりづらくなるという問題がありました。 何年もの開発の後に、Chromium はビルドシステムを gn に切り替えました。こちらの方がはるかに高速で明確なアーキテクチャとなっています。

gn の改良点の一つは、オブジェクトファイルのグループを表す source_set を導入したことです。 gyp では、各モジュールは static_libraryshared_library のいずれかで表現されます。Chromium の通常のビルドでは、各モジュールの静的ライブラリを生成し、最終的な実行ファイルへリンクしていました。 gn を使うと、各モジュールはオブジェクトファイルの集まりだけを生成し、最終的な実行ファイルには全オブジェクトファイルをリンクするだけなので、中間の静的ライブラリファイルは生成されなくなります。

しかし、この改善は libchromiumcontent に大きなお世話でした。なぜなら、中間の静的ライブラリファイルは libchromiumcontent が実際に必要としていたのです。

これを解決する最初の試みは、静的ライブラリファイルを生成するように gn にパッチを当てる ことでした、これで問題は解決しましたが、まともな解決策には程遠いものでした。

2 つ目の試みは、@alespergl による、オブジェクトファイルのリストからカスタムの静的ライブラリを生成する ものでした。 これは、最初にダミービルドを実行して生成されたオブジェクトファイルのリストを収集してから、gn にそのリストを与えて静的ライブラリを実際にビルドするという仕掛けでした。 これは Chromium のソースコードを最小限変更しただけで、Electron のビルドアーキテクチャはそのままです。

概要

このように、Electron を Chromium の一部として構築するのに比べて、ライブラリとして Chromium を構築するにはより多くの労力と継続的なメンテナンスが必要になります。 しかし、後者は Electron のビルドに強力なハードウェアが不要となるため、より多くの開発者が Electron をビルドして貢献できるようになります。 この努力はそれだけの価値があります。

Electron の舞台裏: 弱参照

· 読むのにかかる時間 1 分

ガベージコレクションがある言語 JavaScript は、ユーザーがリソースを手動で管理しなくてよくなります。 しかし、Electron はこの環境をホストしているため、メモリリークとリソースリークの両方を避けようと非常に慎重にならざるをえません。

この記事では、弱参照の概念と、それが Electron のリソース管理でどのように使われているかを紹介します。


弱参照

JavaScript でオブジェクトを変数に代入するというのは、必ずオブジェクトへの参照を追加していることになります。 オブジェクトへの参照がある限り、そのオブジェクトはずっとメモリに保持されます。 オブジェクトへのすべての参照がなくなる、例えばオブジェクトを格納する変数がなくなると、JavaScript エンジンは次のガベージコレクションでそのメモリを回収します。

弱参照とは、ガベージコレクションされるかどうかに影響せずオブジェクトを取得できるようにする参照です。 オブジェクトがガベージコレクションされたときにその通知もされます。 これにより、JavaScript でリソース管理を可能にします。

Electron の NativeImage クラスを例に挙げると、nativeImage.create() APIを呼び出すたびに NativeImage インスタンスが返され、これに画像データが C++ 側で格納されます。 インスタンスの処理が終わり JavaScript エンジン (V8) がオブジェクトをガベージコレクトしたら、C++ のコードが呼び出されてメモリ内の画像データが解放されるので、ユーザが手動で管理する必要はありません。

別の例としては、ウインドウ消失問題 があります。これはウインドウへの参照がすべてなくなったときにガベージコレクトされる様子を、視覚的に観察できます。

Electron での弱参照のテスト

生の JavaScript には弱参照を代入する方法がないので、弱参照を直接テストする方法はありません。 弱参照に関連する JavaScript の API だと WeakMap がありますが、これは弱参照のキーを作成するだけなので、オブジェクトがガベージコレクトされたかどうかは知ることができません。

v0.37.8 以前のバージョンの Electron では、内部の v8Util.setDestructor API を使用して弱参照をテストできました。以下のように、渡されたオブジェクトに弱参照を追加し、オブジェクトがガベージコレクションされたときにコールバックを呼び出すものです。

// 以下のコードは Electron < v0.37.8 でのみ実行できます。
var v8Util = process.atomBinding('v8_util');

var object = {};
v8Util.setDestructor(object, function () {
console.log('The object is garbage collected');
});

// オブジェクトへの参照を全て削除します。
object = undefined;
// GC を手動で起動します。
gc();
// コンソールに "The object is garbage collected" と出力されます。

注意としては、内部の gc 関数を公開させるために、--js-flags="--expose_gc" コマンドスイッチで Electron を起動する必要があります。

この API は後のバージョンで削除されました。このため V8 では実際にはデストラクタで JavaScript コードを実行できず、これをしようとしても確率でクラッシュします。

remote モジュールでの弱参照

C++ でネイティブリソースを管理する以外にも、Electron は JavaScript のリソースを管理するために弱参照が必要です。 例えば、Electron の remote モジュールはいわゆる Remote Procedure Call (RPC) モジュールで、レンダラープロセスからメインプロセス内のオブジェクトを使用できるようにます。

remote モジュールの重要な課題の 1 つに、メモリリークを避けるというものがあります。 ユーザがレンダラープロセス内のリモートオブジェクトを取得する場合、remote モジュールは、レンダラープロセス内の参照がなくなるまでオブジェクトがメインプロセスに存在し続けるよう保証しなければなりません。 さらに、レンダラープロセスでリモートオブジェクトへの参照がなくなったときに、そのオブジェクトがガベージコレクションされるようにする必要があります。

例えば、適切な実装を行わないと、以下のコードはすぐにメモリリークを起こしてしまいます。

const { remote } = require('electron');

for (let i = 0; i < 10000; ++i) {
remote.nativeImage.createEmpty();
}

remote モジュールのリソース管理は単純です。 オブジェクトの要求ごとにメインプロセスへメッセージが送信されます。それに対して Electron はオブジェクトをマップに保存して ID を割り当て、レンダラープロセスにその ID を送り返します。 レンダラープロセスでは、remote モジュールが ID を受け取ってプロキシオブジェクトでラップし、プロキシオブジェクトがガベージコレクションされると、オブジェクト解放のメッセージをメインプロセスに送信します。

remote.require API を例にすると、簡略化した実装は以下のようになります。

remote.require = function (name) {
// モジュールのメタデータを返すようにメインプロセスに伝えます。
const meta = ipcRenderer.sendSync('REQUIRE', name);
// プロキシオブジェクトを作成します。
const object = metaToValue(meta);
// プロキシオブジェクトがガベージコレクションされたときに
// オブジェクトの解放をメインプロセスに指示します。
v8Util.setDestructor(object, function () {
ipcRenderer.send('FREE', meta.id);
});
return object;
};

メインプロセスでは以下のようにします。

const map = {};
const id = 0;

ipcMain.on('REQUIRE', function (event, name) {
const object = require(name);
// オブジェクトへの参照を追加します。
map[++id] = object;
// オブジェクトをメタデータに変換します。
event.returnValue = valueToMeta(id, object);
});

ipcMain.on('FREE', function (event, id) {
delete map[id];
});

弱参照の辞書配列

先述の単純な実装では、remote モジュールを呼び出すたびにメインプロセスが新しいリモートオブジェクトを返し、各リモートオブジェクトがメインプロセスのオブジェクトへの参照を表します。

デザイン自体は問題ないのですが、同じオブジェクトを受信するために複数回呼び出すと、複数のプロキシオブジェクトが作成され、複雑なオブジェクトの場合にメモリ使用量とガベージコレクションを圧迫するという問題があります。

以下のようなコードがあったとします。

const { remote } = require('electron');

for (let i = 0; i < 10000; ++i) {
remote.getCurrentWindow();
}

まずプロキシオブジェクトを作成するためにメモリを多く使用し、そのガベージコレクションと IPC メッセージの送信に CPU(Central Processing Unit) を占有します。

明白な最適化としては、リモートオブジェクトのキャッシュがあります。すなわち、すでに同じ ID のリモートオブジェクトが存在する場合、新しいオブジェクトを作成するのではなく以前のリモートオブジェクトを返すようにします。

これは JavaScript コアの API ではできません。 通常の辞書配列を使ってオブジェクトをキャッシュすれば V8 によるオブジェクトのガベージコレクションを防げますが、WeakMap クラスではオブジェクトのみが弱参照のキーに使えます。

これを解決するために、値を弱参照として持つマップ型を追加しました。ID を持つオブジェクトのキャッシュに最適です。 これで、remote.require は以下のようになります。

const remoteObjectCache = v8Util.createIDWeakMap()

remote.require = function (name) {
// モジュールのメタデータを返すようにメインプロセスに伝えます。
...
if (remoteObjectCache.has(meta.id))
return remoteObjectCache.get(meta.id)
// プロキシオブジェクトを作成します。
...
remoteObjectCache.set(meta.id, object)
return object
}

注意として、remoteObjectCache はオブジェクトを弱参照として保管するので、オブジェクトがガベージコレクトされたときでもキーの削除は不要です。

ネイティブコード

Electron の弱参照の C++ コードに興味がある方は、以下のファイルを参照してください。

setDestructor API:

createIDWeakMap API:

Electron の舞台裏: Node をライブラリとして使用する

· 読むのにかかる時間 1 分

Electron の舞台裏について説明するシリーズ、第二弾です。 イベントループの統合についてまだ読んでいない方は 最初の記事 をご覧ください。

ほとんどの人は Node をサーバサイドアプリケーションに使っていますが、Node の豊富な API セットと活発なコミュニティのおかげで組み込みライブラリにも最適です。 この記事では、Electron のライブラリとして Node がどのように使われているかを解説します。


ビルドシステム

Node も Electron も GYP をビルドシステムとして使用しています。 アプリ内に Node を埋め込みたい場合は、あなたもビルドシステムとして GYP を使用する必要があります。

GYP は初めてですか? そうであれば、この記事を読み進める前に このガイド を読んでからにしてください。

Node のフラグ

Node のソースコードディレクトリにある node.gyp ファイルには、Node をどのように構築するかが記述されており、多くの GYP 変数とともに Node のどの部分を有効にするのか、特定の設定ファイルを開くかどうかを制御しています。

ビルドフラグを変更するには、プロジェクトの .gypi ファイルに変数を設定する必要があります。 Node の configure スクリプトは、いくつかの一般的な設定ファイルを生成できます。例えば、./configure --shared を実行すると、Node を共有ライブラリとしてビルドするように指示する変数を含んだ config.gypi が生成されます。

Electron は独自のビルドスクリプトを持っているので、この configure スクリプトは使いません。 Node の設定は、Electron のルートソースコードディレクトリにある common.gypi ファイルで定義されています。

Electron と Node のリンク

Electron では、GYP 変数 node_sharedtrue に設定することで Node を共有ライブラリとしてリンクしています。このため、Node のビルドタイプは executable から shared_library に変更され、Node の main エントリポイントを含むソースコードはコンパイルされません。

Electron は Chromium に同梱されている V8 ライブラリを使用しているため、Node のソースコードに含まれている V8 ライブラリは使用しません。 これは node_use_v8_platformnode_use_bundled_v8 の両方を false に設定することで実現しています。

共有ライブラリか静的ライブラリか

Node とリンクする際には 2 つの選択肢があります。静的ライブラリとしてビルドし最終的な実行ファイルにインクルードするか、共有ライブラリとしてビルドし最終的な実行ファイルと一緒に頒布するかです。

Electron では、Node は長い間静的ライブラリとしてビルドしていました。 これはビルドがシンプルで、高水準なコンパイラの最適化が可能で、余分な node.dll ファイルいらずで Electron を頒布できました。

しかし、これは Chrome が BoringSSL を使うようになってから変わりました。 BoringSSL は OpenSSL のフォークで、いくつかの未使用の API を削除し、多くの既存のインターフェースを変更しています。 Node は依然 OpenSSL を使用しているため、コンパイラがそれらをリンクすると、矛盾するシンボルのために多数のリンクエラーを発生させてしまいます。

Electron では、Node で BoringSSL を使うことも Chromium で OpenSSL を使うこともできませんでした。そのため、Node を共有ライブラリとしてビルドするように切り替え、BoringSSL と OpenSSL のシンボルをそれぞれのコンポーネントで隠す という選択肢しかありませんでした。

この変化は、Electron にいくぶんかプラスの副作用をもたらしました。 この変更以前は、Windows 上でネイティブモジュールを使用している場合、インポートライブラリ内で実行ファイル名をハードコーディングしていたため、実行ファイル名を変更できませんでした。 Node が共有ライブラリとして構築されてからは、すべてのネイティブモジュールを node.dll にリンクしたため、この制限がなくなりました。

ネイティブモジュールのサポート

Node のネイティブモジュール は、Node がロードするエントリ関数を定義し、Node から V8 や libuv のシンボルを検索することで動作します。 これは組み込み開発者にとっては少し面倒です。なぜなら、デフォルトではライブラリとして Node をビルドする際に V8 と libuv のシンボルが隠されており、ネイティブモジュールはシンボルを見つけられずロードに失敗するからです。

そこで、ネイティブモジュールを動作させるために Electron では V8 と libuv のシンボルを公開しました。 V8 では、Chromium の設定ファイル内の全シンボルを強制的に公開する ことで実現しています。 libuv の場合、BUILDING_UV_SHARED=1 定義を設定する ことで実現しています。

アプリで Node を起動する

Node をビルドしてリンクする全作業の後は、最後の段階としてアプリで Node を実行します。

Node は、自分自身を他のアプリに組み込むための公開 API は多く提供していません。 通常は、node::Startnode::Init を呼び出すだけで Node の新しいインスタンスを起動できます。 しかし、Node ベースで複雑なアプリを構築する場合は、node::CreateEnvironment のような API を使用して全ステップを正確に制御する必要があります。

Electron で Node を起動する際には、公式の Node バイナリに近いメインプロセスで動作するスタンドアロンモードと、ウェブページに Node API を挿入する組み込みモードの、2 つのモードがあります。 この詳細は後々の記事で解説する予定です。

Electron の舞台裏: メッセージループの統合

· 読むのにかかる時間 1 分

Electron の舞台裏について説明するシリーズ、第一弾です。 この投稿では、 Electron が Node のイベントループをどのように Chromium と統合しているかを紹介します。


これまで、Node を GUI プログラミングに使う試みは数多くありました。GTK+ のバインディングでは node-gui が、 Qt のバインディングでは node-qt があります。 しかし、GUI ツールキットは独自のメッセージループを持っているにもかかわらず、Node は独自のイベントループに libuv を使用しています。メインスレッドは同時に 1 つのループしか実行できないため、本番環境ではどちらも動作しません。 そのために、Node で GUI のメッセージループを共通化して実行するための仕掛けとして非常に短い間隔のタイマーでメッセージループをポンピングすると、GUI のレスポンスが遅くなり、多くの CPU リソースを占有してしまいます。

Electron の開発中にも同じ問題が発生しましたが、 Node のイベントループを Chromium のメッセージループに統合するという、逆の方法を取りました。

メインプロセスとレンダラープロセス

メッセージループの統合についての詳細の前に、 Chromium のマルチプロセスアーキテクチャについて説明します。

Electron には、メインプロセスとレンダラープロセス、2 種類のプロセスがあります (これはとても単純化してあります。詳細は マルチプロセスアーキテクチャ を参照してください)。 メインプロセスはウインドウの作成など GUI が動作するための責務を担い、レンダラープロセスはウェブページの実行とレンダリングだけを行います。

Electron では JavaScript を使ってメインプロセスとレンダラープロセスの両方を制御できるように、両方のプロセスに Node を統合する必要があるのです。

Chromium のメッセージループを libuv に置換

最初の試みは、 Chromium のメッセージループを libuv で再実装することでした。

レンダラープロセスは、メッセージループはファイル記述子とタイマーだけをリッスンしていたので簡単でした。

しかし、メインプロセスではとても困難でした。 各プラットフォームは独自の GUI メッセージループを持ちます。 macOS の Chromium は NSRunLoop を使う一方、 Linux では glib を使います。 ネイティブ GUI のメッセージループからファイル記述子を抽出して libuv の繰り返しに与えるために多くのハックを試みましたが、それでもうまくいかないエッジケースに遭遇しました。

最終的に、短い間隔で GUI メッセージループをポーリングするタイマーを追加しました。 この結果、プロセスは一定の CPU 使用率を消費し、操作によっては長い遅延が発生してしまいました。

別のスレッドで Node のイベントループをポーリング

libuv が成熟するにつれて、別のアプローチができるようになりました。

libuv にバックエンドファイル記述子の概念が導入されました。これは libuv がイベントループのためにポーリングするファイル記述子 (またはハンドル) です。 バックエンドファイル記述子をポーリングすることで、 libuv で新しいイベントが発生したときに通知を受けられるようになりました。

そこで Electron では、バックエンドファイル記述子をポーリングするために別のスレッドを作成しました。これは libuv API の代わりにシステムコールでポーリングしていたのでスレッドセーフでした。 そして、 libuv のイベントループで新しいイベントがあるとき、メッセージが Chromium のメッセージループに送信され、 libuv のイベントはメインスレッドで処理されるようになりました。

このようにして、 Chromium や Node にパッチを当てることを避けつつ、メインプロセスとレンダラープロセスで同じコードを使用できました。

コード

メッセージループの統合の実装は electron/atom/common/ 下の node_bindings ファイルで見ることができます。 これは Node を統合したいプロジェクトでも簡単に再利用できます。

更新: 実装を electron/shell/common/node_bindings.cc に移動しました。