メインコンテンツへスキップ

「Electron Internals」タグの付いた記事6件

「Electronのソースコードを深く掘り下げる技術解説」

すべてのタグを見る

WebView2とElectron

·6 分で読めます

ここ数週間、新しいWebView2とElectronの違いについていくつか質問がありました。

両チームは、デスクトップ上でWebテクノロジーを最高の状態にすることを明確な目標としており、包括的な比較について話し合われています。

ElectronとWebView2は、急速に進歩し、常に進化しているプロジェクトです。現在のElectronとWebView2の類似点と相違点の簡単なスナップショットをまとめました。


アーキテクチャの概要

ElectronとWebView2はどちらも、Webコンテンツをレンダリングするために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つ以上のレンダラープロセスと通信する単一のメインプロセス)を継承しています。これらのプロセスは、システム上で実行されている他のアプリケーションとは完全に分離されています。すべてのElectronアプリケーションは、ルートブラウザプロセス、いくつかのユーティリティプロセス、およびゼロ以上のレンダリングプロセスを含む、個別のプロセスのツリーです。同じユーザーデータフォルダー(アプリのスイートのように)を使用するWebView2アプリは、レンダラー以外のプロセスを共有します。異なるデータフォルダーを使用するWebView2アプリは、プロセスを共有しません。

  • ElectronJSプロセスモデル

    ElectronJS Process Model Diagram

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

    WebView2 Process Model Diagram

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

Electronは、メニュー、ファイルシステムアクセス、通知など、一般的なデスクトップアプリケーションのニーズに対応するAPIを提供します。WebView2は、WinForms、WPF、WinUI、またはWin32などのアプリケーションフレームワークに統合することを目的としたコンポーネントです。WebView2は、JavaScriptを介したWeb標準以外のオペレーティングシステムAPIを提供しません。

Node.jsはElectronに統合されています。Electronアプリケーションは、レンダラープロセスとメインプロセスから、任意のNode.js API、モジュール、またはnode-native-addonを使用できます。WebView2アプリケーションは、アプリケーションの残りの部分がどの言語またはフレームワークで記述されているかを想定していません。JavaScriptコードは、アプリケーションホストプロセスを介してオペレーティングシステムへのアクセスをプロキシする必要があります。

Electronは、Fuguプロジェクトから開発されたAPIを含む、Web APIとの互換性を維持するよう努めています。 ElectronのFugu API互換性のスナップショットがあります。WebView2は、EdgeとのAPIの違いの同様のリストを維持しています。

Electronには、フルアクセスからフルサンドボックスまで、Webコンテンツ用の構成可能なセキュリティモデルがあります。WebView2コンテンツは常にサンドボックス化されています。Electronには、セキュリティモデルの選択に関する包括的なセキュリティドキュメントがあります。WebView2には、セキュリティのベストプラクティスもあります。

ElectronのソースはGitHubで管理および公開されています。アプリケーションは、独自のブランドのElectronを構築できます。WebView2のソースはGitHubでは公開されていません。

簡単なまとめ

ElectronWebView2
ビルド依存関係ChromiumEdge
GitHubでソースを公開はいいいえ
Edge/Chrome DLLを共有いいえはい(Edge 90時点)
アプリケーション間の共有ランタイムいいえオプション
アプリケーションAPIはいいいえ
Node.jsはいいいえ
サンドボックスオプション常に
アプリケーションフレームワークが必要いいえはい
サポートされるプラットフォームMac、Win、LinuxWin (Mac/Linux計画中)
アプリ間でのプロセス共有なしオプション
フレームワークの更新管理アプリケーションWebView2

パフォーマンスに関する考察

Webコンテンツのレンダリングに関しては、Electron、WebView2、およびその他のChromiumベースのレンダラーの間でパフォーマンスの違いはほとんどないと考えています。潜在的なパフォーマンスの違いを調査することに興味がある人のために、Electron、C++ + WebView2、およびC# + WebView2を使用して構築されたアプリの足場を作成しました

Webコンテンツのレンダリング以外で考慮すべきいくつかの違いがあり、Electron、WebView2、Edgeなどの関係者は、PWAを含む詳細な比較に取り組むことに関心を示しています。

プロセス間通信(IPC)

Electronアプリではパフォーマンスに影響することが多いため、すぐに強調したい違いが1つあります。

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

ElectronとWebView2は類似のプロセスモデルを持っていますが、基盤となるIPCは異なります。JavaScriptとC++またはC#間の通信には、マーシャリングが必要であり、最も一般的な形式はJSON文字列への変換です。JSONのシリアライズ/パースはコストの高い操作であり、IPCのボトルネックはパフォーマンスに悪影響を与える可能性があります。Edge 93以降、WV2はネットワークイベントにCBORを使用します。

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

まとめ

ElectronとWebView2にはいくつかの違いがありますが、ウェブコンテンツのレンダリングに関しては大きな違いはないでしょう。結局のところ、アプリのアーキテクチャとJavaScriptライブラリ/フレームワークがメモリとパフォーマンスに最も大きな影響を与えます。なぜなら、Chromiumはどこで実行されてもChromiumだからです。

この投稿をレビューし、WebView2アーキテクチャの最新の見解を提供してくれたWebView2チームに感謝します。彼らはプロジェクトへのフィードバックを歓迎しています。

ElectronにおけるネイティブからJavaScriptへ

·4分で読めます

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');

この行は、ElectronのC++/Objective-CモジュールをJavaScriptにバインドして開発者が使用できるようにするメカニズムを直接示しています。この関数は、ヘッダーとElectronBindingsクラスの実装ファイルによって作成されます。

process.electronBinding

これらのファイルは、Node.jsのprocess.bindingと同様に動作するprocess.electronBinding関数を追加します。process.bindingは、Node.jsのrequire()メソッドの低レベルの実装ですが、JSで記述された他のコードの代わりにネイティブコードをrequireできます。このカスタムのprocess.electronBinding関数は、Electronからネイティブコードをロードする機能を提供します。

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

native_mate

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

native_mate/native_mate内には、object_template_builderのヘッダーファイルと実装ファイルがあります。これにより、ネイティブコードでモジュールを形成し、その形状がJavaScript開発者が期待する形に適合させることができます。

mate::ObjectTemplateBuilder

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

これを実際に確認するには、appモジュールの実装ファイルであるatom_api_app.ccを参照してください。下部に次のものがあります。

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

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

.SetMethod("method_name", &function_to_bind)

これはJavaScriptで次のものに相当します。

function App{}
App.prototype.getGPUInfo = function () {
// implementation here
}

このクラスには、モジュールにプロパティを設定する関数も含まれています。

.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
}
})

および

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

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

特定のモジュールメソッドを実装する場所の決定は、それ自体が複雑で非決定論的なものであり、これについては今後の投稿で説明します。

Electron Internals:ライブラリとしてのChromiumの構築

·7分で読めます

Electronは、必ずしも他のプロジェクトで使用されるように設計されていないGoogleのオープンソースChromiumに基づいています。この記事では、ChromiumがElectronで使用するためのライブラリとしてどのように構築され、ビルドシステムが長年にわたってどのように進化してきたかを紹介します。


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に基づいて最小限のブラウザを簡単にビルドできます。ブラウザインターフェースを提供するコアモジュールは、Content Moduleと呼ばれます。

Content Moduleでプロジェクトを開発する場合、最も簡単な方法は、Chromiumの一部としてプロジェクトをビルドすることです。これは、まずChromiumのソースコードをチェックアウトし、次にプロジェクトをChromiumのDEPSファイルに追加することで実行できます。

NW.jsとElectronの非常に初期のバージョンでは、この方法でビルドしていました。

欠点は、Chromiumは非常に大規模なコードベースであり、ビルドには非常に強力なマシンが必要になることです。通常のラップトップの場合、5時間以上かかる可能性があります。そのため、プロジェクトに貢献できる開発者の数に大きな影響を与え、開発も遅くなります。

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

Content Moduleのユーザーとして、Electronはほとんどの場合Chromiumのコードを変更する必要がないため、Electronのビルドを改善する明らかな方法は、Chromiumを共有ライブラリとしてビルドし、それをElectronでリンクすることです。この方法では、開発者はElectronに貢献する際にChromium全体をビルドする必要がなくなります。

libchromiumcontentプロジェクトは、この目的のために@arobenによって作成されました。これは、ChromiumのContent Moduleを共有ライブラリとしてビルドし、ダウンロード用のChromiumヘッダーとプリビルドバイナリを提供します。libchromiumcontentの初期バージョンのコードは、このリンクにあります。

brightrayプロジェクトもlibchromiumcontentの一部として誕生し、Content Moduleの周りに薄いレイヤーを提供します。

libchromiumcontentとbrightrayを一緒に使用することで、開発者はChromiumのビルドの詳細に立ち入ることなく、ブラウザをすばやくビルドできます。また、プロジェクトのビルドに高速ネットワークと強力なマシンは必要ありません。

Electronとは別に、Breachブラウザのように、この方法で構築された他のChromiumベースのプロジェクトもありました。

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

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

解決策は、DLLファイルの生成時に不要なシンボルをフィルタリングすることでした。これは、リンカーに.defファイルを提供し、スクリプトを使用して名前空間下のシンボルをエクスポートするかどうかを判断することで機能しました。

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

コンポーネントビルド

libchromiumcontentで実行された次のステップについて説明する前に、まずChromiumのコンポーネントビルドの概念を紹介することが重要です。

巨大なプロジェクトとして、Chromiumではビルド時にリンクステップに非常に時間がかかります。通常、開発者が小さな変更を加えた場合、最終出力が表示されるまでに10分かかることがあります。これを解決するために、Chromiumはコンポーネントビルドを導入しました。これにより、Chromiumの各モジュールが個別の共有ライブラリとしてビルドされるため、最終リンクステップに費やす時間が目立たなくなります。

生のバイナリの出荷

Chromiumが成長し続けるにつれて、Chromiumには非常に多くのエクスポートされたシンボルがあり、Content Moduleと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は非常に高速で、明確なアーキテクチャを持っています。

gnの改善点の1つは、オブジェクトファイルのグループを表すsource_setを導入したことです。gypでは、各モジュールはstatic_libraryまたはshared_libraryのいずれかで表され、Chromiumの通常ビルドでは、各モジュールがスタティックライブラリを生成し、それらが最終的な実行可能ファイルにリンクされていました。gnを使用することにより、各モジュールはオブジェクトファイルの束を生成するだけで、最終的な実行可能ファイルはすべてのオブジェクトファイルをリンクするだけになりました。これにより、中間スタティックライブラリファイルは生成されなくなりました。

しかし、この改善はlibchromiumcontentに大きな問題を引き起こしました。なぜなら、中間スタティックライブラリファイルがlibchromiumcontentに実際に必要だったからです。

これを解決するための最初の試みは、スタティックライブラリファイルを生成するようにgnにパッチを適用することでした。これにより問題は解決されましたが、適切な解決策とは言えませんでした。

2番目の試みは、@alesperglによって、オブジェクトファイルのリストからカスタムスタティックライブラリを生成するという形で行われました。まず、ダミービルドを実行して生成されたオブジェクトファイルのリストを収集し、次にそのリストをgnに供給することでスタティックライブラリを実際にビルドするというトリックを使用しました。これにより、Chromiumのソースコードへの変更を最小限に抑え、Electronのビルドアーキテクチャを維持することができました。

まとめ

見てわかるように、ElectronをChromiumの一部としてビルドするのと比較して、Chromiumをライブラリとしてビルドするには、より多くの労力と継続的なメンテナンスが必要です。しかし、後者の方がElectronをビルドするための高性能なハードウェアの要件がなくなり、より多くの開発者がElectronをビルドして貢献できるようになります。この努力は完全に価値があります。

Electron Internals:弱い参照

·6 分で読めます

ガベージコレクションを備えた言語であるJavaScriptは、ユーザーを手動でのリソース管理から解放します。しかし、Electronはこの環境をホストしているため、メモリとリソースの両方のリークを回避するように非常に注意する必要があります。

この投稿では、弱参照の概念と、Electronでリソースを管理するために弱参照がどのように使用されるかを紹介します。


弱参照

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

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

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

別の例としては、ウィンドウが消える問題があります。これは、ウィンドウへのすべての参照がなくなると、ウィンドウがガベージコレクションされる様子を視覚的に示しています。

Electronでの弱参照のテスト

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

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

// Code below can only run on Electron < v0.37.8.
var v8Util = process.atomBinding('v8_util');

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

// Remove all references to the object.
object = undefined;
// Manually starts a GC.
gc();
// Console prints "The object is garbage collected".

内部のgc関数を公開するには、--js-flags="--expose_gc"コマンドスイッチを付けてElectronを起動する必要があることに注意してください。

このAPIは、V8が実際にはデストラクタ内でJavaScriptコードを実行することを許可しておらず、後のバージョンで実行するとランダムなクラッシュが発生するため、後のバージョンで削除されました。

remoteモジュールでの弱参照

C++でネイティブリソースを管理するだけでなく、ElectronはJavaScriptリソースを管理するために弱参照も必要です。その例として、Electronのremoteモジュールがあります。これは、リモートプロシージャコール(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) {
// Tell the main process to return the metadata of the module.
const meta = ipcRenderer.sendSync('REQUIRE', name);
// Create a proxy object.
const object = metaToValue(meta);
// Tell the main process to free the object when the proxy object is garbage
// collected.
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);
// Add a reference to the object.
map[++id] = object;
// Convert the object to metadata.
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(中央処理装置)を占有します。

明らかな最適化は、リモートオブジェクトをキャッシュすることです。同じIDのリモートオブジェクトが既にある場合は、新しいオブジェクトを作成するのではなく、以前のリモートオブジェクトが返されます。

これは、JavaScriptコアのAPIでは不可能です。オブジェクトをキャッシュするために通常のマップを使用すると、V8がオブジェクトをガベージコレクションするのを防ぎ、一方、WeakMapクラスはオブジェクトを弱キーとしてのみ使用できます。

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

const remoteObjectCache = v8Util.createIDWeakMap()

remote.require = function (name) {
// Tell the main process to return the meta data of the module.
...
if (remoteObjectCache.has(meta.id))
return remoteObjectCache.get(meta.id)
// Create a proxy object.
...
remoteObjectCache.set(meta.id, object)
return object
}

remoteObjectCacheはオブジェクトを弱参照として格納するため、オブジェクトがガベージコレクションされたときにキーを削除する必要がないことに注意してください。

ネイティブコード

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

setDestructor API

createIDWeakMap API

Electron Internals:ライブラリとしてのNodeの使用

·4分で読めます

これは、Electronの内部構造を説明する継続的なシリーズの2番目の投稿です。まだ読んでいない場合は、イベントループ統合に関する最初の投稿を確認してください。

ほとんどの人がサーバーサイドアプリケーションにNodeを使用していますが、Nodeの豊富なAPIセットと活気のあるコミュニティにより、組み込みライブラリにも最適です。この投稿では、NodeがElectronでライブラリとしてどのように使用されているかを説明します。


ビルドシステム

NodeとElectronはどちらもビルドシステムとしてGYPを使用しています。アプリにNodeを組み込む場合は、ビルドシステムとしてもそれを使用する必要があります。

GYPを初めて使う方は、この記事の続きに進む前にこちらのガイドをお読みください。

Nodeのフラグ

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

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

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

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つのオプションがあります。Nodeを静的ライブラリとしてビルドし、最終的な実行可能ファイルに含めるか、Nodeを共有ライブラリとしてビルドし、最終的な実行可能ファイルと一緒に配布することができます。

Electronでは、Nodeは長い間静的ライブラリとしてビルドされていました。これにより、ビルドが簡単になり、コンパイラの最適化が最大限に活用され、Electronを余分なnode.dllファイルなしで配布できました。

しかし、ChromeがBoringSSLの使用に切り替えた後、この状況は変わりました。BoringSSLは、いくつかの未使用のAPIを削除し、既存のインターフェイスを多数変更したOpenSSLのフォークです。NodeはまだOpenSSLを使用しているため、コンパイラは、それらが一緒にリンクされると、競合するシンボルが原因で多数のリンクエラーを生成します。

ElectronはNodeでBoringSSLを使用することも、ChromiumでOpenSSLを使用することもできなかったため、唯一の選択肢は、Nodeを共有ライブラリとしてビルドし、それぞれのコンポーネントでBoringSSLとOpenSSLのシンボルを隠すことでした。

この変更により、Electronにいくつかの良い副作用がありました。この変更前は、ネイティブモジュールを使用している場合、実行可能ファイルの名前がインポートライブラリにハードコードされていたため、WindowsでElectronの実行可能ファイルの名前を変更できませんでした。Nodeが共有ライブラリとしてビルドされるようになった後、すべてのネイティブモジュールがnode.dllにリンクされるため、この制限はなくなりました。node.dllの名前は変更する必要がないためです。

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

Nodeのネイティブモジュールは、Nodeがロードするためのエントリ関数を定義し、NodeからV8とlibuvのシンボルを検索することで機能します。V8とlibuvのシンボルは、Nodeをライブラリとしてビルドするときにデフォルトで非表示になっているため、ネイティブモジュールはシンボルを見つけることができず、ロードに失敗するため、これは組み込みを行う人にとっては少し厄介です。

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

アプリでのNodeの起動

Nodeのビルドとリンクのすべての作業が終わったら、最後のステップはアプリでNodeを実行することです。

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

Electronでは、Nodeは2つのモードで起動します。1つは、公式のNodeバイナリに似たメインプロセスで実行されるスタンドアロンモード、もう1つは、WebページにNode APIを挿入する組み込みモードです。これの詳細については、今後の投稿で説明します。

Electron Internals:メッセージループの統合

·3分で読めます

これは、Electronの内部構造を説明するシリーズの最初の投稿です。この記事では、ElectronでNodeのイベントループがChromiumとどのように統合されているかを紹介します。


GUIプログラミングにNodeを使用する試みは多数ありました。たとえば、GTK+バインディング用のnode-guiや、QTバインディング用のnode-qtなどです。しかし、これらのどれも実用にはなりませんでした。GUIツールキットには独自のメッセージループがあり、Nodeは独自のイベントループにlibuvを使用しているため、メインスレッドは一度に1つのループしか実行できないからです。そのため、NodeでGUIメッセージループを実行する一般的な手法は、非常に短い間隔でタイマーでメッセージループをポンプすることです。これにより、GUIインターフェイスの応答が遅くなり、多くのCPUリソースが消費されます。

Electronの開発中に、私たちは同じ問題に逆の形で直面しました。NodeのイベントループをChromiumのメッセージループに統合する必要があったのです。

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

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

Electronには、メインプロセスとレンダラープロセスの2種類のプロセスがあります(実際にはこれは非常に簡略化されており、完全な説明についてはマルチプロセスアーキテクチャを参照してください)。メインプロセスはウィンドウの作成などのGUI作業を担当し、レンダラープロセスはWebページの実行とレンダリングのみを行います。

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

Chromiumのメッセージループをlibuvで置き換える

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

レンダラープロセスでは、メッセージループがファイル記述子とタイマーのみをリッスンしていたため、簡単でした。libuvとのインターフェイスを実装するだけで済みました。

しかし、メインプロセスでは非常に困難でした。プラットフォームごとに独自の種類のGUIメッセージループがあります。macOS ChromiumはNSRunLoopを使用し、Linuxはglibを使用します。ネイティブGUIメッセージループから基盤となるファイル記述子を抽出して、それらを反復処理のためにlibuvに渡すという、多くのハックを試みましたが、それでも機能しないエッジケースが発生しました。

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

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

libuvが成熟するにつれて、別のアプローチをとることが可能になりました。

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

そこでElectronでは、バックエンドfdをポーリングするために別のスレッドを作成しました。libuv APIの代わりにシステムコールを使用してポーリングしていたため、スレッドセーフでした。そして、libuvのイベントループに新しいイベントが発生するたびに、メッセージがChromiumのメッセージループにポストされ、libuvのイベントがメインスレッドで処理されます。

このようにして、ChromiumとNodeにパッチを適用することを避け、メインプロセスとレンダラープロセスの両方で同じコードが使用されました。

コード

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

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