Electron APIの内部を覗いてみる

クロスプラットフォームなデスクトップアプリ開発ができるElectronですが、よりデスクトップアプリらしさを追求していくとクロスプラットフォームなどというものはやはり幻想だったということ思い出します。

- プラットフォームごとにアプリに一般的に期待される挙動が違う
- プラットフォームごとにElectronの挙動が微妙に違う
- 機能の実現にそもそものElectronのAPIが足りない

そんなことがあってAPIを追加したいと思っていたことがあり、ネイティブのAPIを呼び出すべくElectronのソースコードを調べた時のことを書き並べたいと思います。なお、この記事ではElectron 1.4.12を扱います。

## 知っていると役に立つもの
- native addonの作成方法とパターン
    - https://nodejs.org/api/addons.html
- Electronのデバッグ方法
    - http://electron.atom.io/docs/development/debug-instructions-windows/
    - http://electron.atom.io/docs/development/debugging-instructions-macos/

## コードの構成
ドキュメントの[Source Code Directory Structure](http://electron.atom.io/docs/development/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/browser/api/power-monitor.js](https://github.com/electron/electron/blob/v1.4.12/lib/browser/api/power-monitor.js)

### `lib/*/api/exports/electron.js`
各APIがexportされ、我々が普段利用している`electron`モジュールから使えるようになります。MainプロセスとRendererプロセスのどちらでAPIが使えるのかは、最終的にこの部分で決まるようです。
例えば、両方のプロセスから呼べることになっている`screen`は、[Renderer側では内部で`remote`を使ってMain側の`screen`を呼び出す](https://github.com/electron/electron/blob/v1.4.12/lib/renderer/api/screen.js)実装になっており、APIの区分とは一体何だったのか...という気分になれます。

### `process.atomBinding()`
ネイティブへのバインディングです。ここで引数になる文字列が、どのネイティブ実装にバインディングされるかを選択するようになっているようで、後のネイティブの実装で使います。

## ネイティブ実装
`atom/`にはElectronのネイティブ部分の実装が含まれます。まず、先の`process.atomBinding()`の引数に使った文字列を使って`REFERENCE_MODULE`を列挙しておきます。

- [atom/common/node_bindings.cc#L34-L62](https://github.com/electron/electron/blob/v1.4.12/atom/common/node_bindings.cc#L34-L62)


### 関数・クラスの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を使ったコードのイメージが頭に入っていると理解しやすいです。

- 例: [atom/browser/api/atom_api_dialog.cc#L112-L123](https://github.com/electron/electron/blob/v1.4.12/atom/browser/api/atom_api_dialog.cc#L112-L123)

native addonと異なる点としては、exportされる関数の中で引数をいちいち`FunctionCallbackInfo`から取り出す必要がなく、素直にC++の型を使って関数宣言できるという点が挙げられます。`dict.SetMethod()`がテンプレート関数になっていて、[electron/native-mate](https://github.com/electron/native-mate)がいい感じに自動的に引数と戻り値の変換処理を追加しているためのようです。もし必要な型への変換がなければ`mate::Converter::FromV8()`や`mate::Converter::ToV8()`を実装することで対応できます。


- 例: [atom/browser/api/atom_api_dialog.cc#L91-L96](https://github.com/electron/electron/blob/v1.4.12/atom/browser/api/atom_api_dialog.cc#L91-L96)
- 例: [atom/browser/api/atom_api_dialog.cc#L20-L38](https://github.com/electron/electron/blob/v1.4.12/atom/browser/api/atom_api_dialog.cc#L20-L38)

### オブジェクト
JavaScriptの世界からV8の世界を経て、ようやくC++の世界にたどり着きました。ここまで来ると素直なC++を書くことができます。しかし現実的には、プラットフォームのAPIを呼び出すとなると何かしらのオブジェクトを扱い、さらにそれをJavaScriptの世界へ送り出すことは避けられないでしょう。

- Win32 APIのHANDLE
- [WRL](https://msdn.microsoft.com/ja-jp/library/hh438466.aspx)でラップしたオブジェクト (WindowsランタイムAPIをWin32アプリから呼ぶために必要)
- Objective-Cのオブジェクト (Cocoa APIを呼ぶために必要)
- etc.

ネイティブのオブジェクトを保持するAPIの実装としては`Tray`や`nativeImage`が参考になったと記憶しています。おおまかには以下のようなポイントがありました。

- V8へexportするために`mate`名前空間のクラスから派生クラスを作る [atom/browser/api/atom_api_tray.h](https://github.com/electron/electron/blob/v1.4.12/atom/browser/api/atom_api_tray.h)
- ↑から呼び出されてV8に関係なく動作する、実体になるクラスを作る [atom/browser/ui/tray_icon.h](https://github.com/electron/electron/blob/v1.4.12/atom/browser/ui/tray_icon.h)
  - コンストラクタをprotectedにしてファクトリ関数を使う
  - ファクトリ関数内ではnewでオブジェクトを確保する
  - 非同期イベントを通知するためのObserverを作る [atom/browser/ui/tray_icon_observer.h](https://github.com/electron/electron/blob/v1.4.12/atom/browser/ui/tray_icon_observer.h)
- macOS用の実装ではWindows/Linux用クラスの派生クラスを作る [atom/browser/ui/tray_icon_cocoa.h](https://github.com/electron/electron/blob/v1.4.12/atom/browser/ui/tray_icon_cocoa.h)
  - ファクトリ関数をObjective-C++で実装する (Objective-C++のヘッダファイルをC++側でインクルードしないようにする)

## おわりに
自分に必要だった部分の書き出しのため端折り気味かつ表面的ではありますが、Electronで動作するJavaScriptからどうやって各プラットフォームのネイティブなAPIを呼び出しているかをまとめました。

そもそもネイティブを触りたいだけならnative addonでいいんじゃないのか、ということも考えられて、何でもかんでもElectron本体に乗せるのはあまりよろしくないと思います。しかし、やはりElectronに持ってもらいたい部分もあるかと思いますし、根の深そうな問題に遭遇すれば内部を調べるほかなくなるわけで、そんなことがあったときに助けになればと思います。