クロスプラットフォームなデスクトップアプリ開発ができるElectronですが、よりデスクトップアプリらしさを追求していくとクロスプラットフォームなどというものはやはり幻想だったということ思い出します。
- プラットフォームごとにアプリに一般的に期待される挙動が違う
- プラットフォームごとにElectronの挙動が微妙に違う
- 機能の実現にそもそものElectronのAPIが足りない
そんなことがあってAPIを追加したいと思っていたことがあり、ネイティブのAPIを呼び出すべくElectronのソースコードを調べた時のことを書き並べたいと思います。なお、この記事ではElectron 1.4.12を扱います。
知っていると役に立つもの
- native addonの作成方法とパターン
- Electronのデバッグ方法
コードの構成
ドキュメントのSource Code Directory Structureにまとまっています。今回見る必要があるのは下の2つです。
-
atom/
- C++ソースコード -
lib/*/api/
- JavaScript APIの実装
JavaScriptに近い側から覗いてみることにしましょう。
JavaScript API
lib/*/api/
にはElectronのAPIのうち、JavaScriptでの実装・ネイティブへのバインディング・APIのexportが実装されています。
また、EventEmitterとして使うAPIへのプロトタイプ追加もここで行います。(.on()
など)
lib/*/api/exports/electron.js
各APIがexportされ、我々が普段利用しているelectron
モジュールから使えるようになります。MainプロセスとRendererプロセスのどちらでAPIが使えるのかは、最終的にこの部分で決まるようです。
例えば、両方のプロセスから呼べることになっているscreen
は、Renderer側では内部でremote
を使ってMain側のscreen
を呼び出す実装になっており、APIの区分とは一体何だったのか...という気分になれます。
process.atomBinding()
ネイティブへのバインディングです。ここで引数になる文字列が、どのネイティブ実装にバインディングされるかを選択するようになっているようで、後のネイティブの実装で使います。
ネイティブ実装
atom/
にはElectronのネイティブ部分の実装が含まれます。まず、先のprocess.atomBinding()
の引数に使った文字列を使ってREFERENCE_MODULE
を列挙しておきます。
関数・クラスのexport
APIの種類によってさらにフォルダが分かれます。
-
atom/browser/api
- Mainプロセス -
atom/common/api
- 両方のプロセス -
atom/renderer/api
- Rendererプロセス
各apiフォルダの中にあるatom_api_*.h
とatom_api_*.cc
がAPIのエントリポイントになっていて、Node.jsのnative addonのように初期化と関数/クラスのexportを行い、V8の世界とC++の世界でやり取りをできるようにします。このとき、先ほどREFERENCE_MODULE()
で列挙した名前を使います。書き方の雰囲気が似ているので、native addonの作成方法やNANを使ったコードのイメージが頭に入っていると理解しやすいです。
native addonと異なる点としては、exportされる関数の中で引数をいちいちFunctionCallbackInfo
から取り出す必要がなく、素直にC++の型を使って関数宣言できるという点が挙げられます。dict.SetMethod()
がテンプレート関数になっていて、electron/native-mateがいい感じに自動的に引数と戻り値の変換処理を追加しているためのようです。もし必要な型への変換がなければmate::Converter::FromV8()
やmate::Converter::ToV8()
を実装することで対応できます。
オブジェクト
JavaScriptの世界からV8の世界を経て、ようやくC++の世界にたどり着きました。ここまで来ると素直なC++を書くことができます。しかし現実的には、プラットフォームのAPIを呼び出すとなると何かしらのオブジェクトを扱い、さらにそれをJavaScriptの世界へ送り出すことは避けられないでしょう。
- Win32 APIのHANDLE
- WRLでラップしたオブジェクト (WindowsランタイムAPIをWin32アプリから呼ぶために必要)
- Objective-Cのオブジェクト (Cocoa APIを呼ぶために必要)
- etc.
ネイティブのオブジェクトを保持するAPIの実装としてはTray
やnativeImage
が参考になったと記憶しています。おおまかには以下のようなポイントがありました。
- V8へexportするために
mate
名前空間のクラスから派生クラスを作る atom/browser/api/atom_api_tray.h - ↑から呼び出されてV8に関係なく動作する、実体になるクラスを作る atom/browser/ui/tray_icon.h
- コンストラクタをprotectedにしてファクトリ関数を使う
- ファクトリ関数内ではnewでオブジェクトを確保する
- 非同期イベントを通知するためのObserverを作る atom/browser/ui/tray_icon_observer.h
- macOS用の実装ではWindows/Linux用クラスの派生クラスを作る atom/browser/ui/tray_icon_cocoa.h
- ファクトリ関数をObjective-C++で実装する (Objective-C++のヘッダファイルをC++側でインクルードしないようにする)
おわりに
自分に必要だった部分の書き出しのため端折り気味かつ表面的ではありますが、Electronで動作するJavaScriptからどうやって各プラットフォームのネイティブなAPIを呼び出しているかをまとめました。
そもそもネイティブを触りたいだけならnative addonでいいんじゃないのか、ということも考えられて、何でもかんでもElectron本体に乗せるのはあまりよろしくないと思います。しかし、やはりElectronに持ってもらいたい部分もあるかと思いますし、根の深そうな問題に遭遇すれば内部を調べるほかなくなるわけで、そんなことがあったときに助けになればと思います。